mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
refactor(portals): create nx feature libraries for xtream and stalker
- Added StalkerCatalogFacadeService to manage Stalker catalog operations. - Created stalker-feature-loaders to define route options for Stalker features. - Removed deprecated stalker.routes.ts file. - Updated shared utilities to include portal catalog facade interfaces. feat(xtream): implement XtreamCatalogFacadeService and related feature loaders - Added XtreamCatalogFacadeService to manage Xtream catalog operations. - Created xtream-feature-loaders to define route options for Xtream features. - Removed deprecated xtream.routes.ts file. - Updated shared utilities to include portal catalog facade interfaces. refactor: organize portal feature libraries - Created separate feature libraries for Stalker and Xtream. - Defined project structure and TypeScript configurations for both libraries. - Implemented routing configurations for Stalker and Xtream features.
This commit is contained in:
1 parent
7f67962dd7
commit
b5bd1bd92a
22 files changed
+973
-512
No files matched your search
@@ -1,6 +1,8 @@
|
||||
import { Route, Routes } from '@angular/router';
|
||||
import { stalkerRoutes } from './stalker/stalker.routes';
|
||||
import { xtreamRoutes } from './xtream-electron/xtream.routes';
|
||||
import { createStalkerRoutes } from '@iptvnator/portal/stalker/feature';
|
||||
import { createXtreamRoutes } from '@iptvnator/portal/xtream/feature';
|
||||
import { stalkerFeatureRouteOptions } from './stalker/stalker-feature-loaders';
|
||||
import { xtreamFeatureRouteOptions } from './xtream-electron/xtream-feature-loaders';
|
||||
|
||||
function withWorkspaceLayout(routes: Route[]): Route[] {
|
||||
return routes.map((route) => ({
|
||||
@@ -80,8 +82,12 @@ export const routes: Routes = [
|
||||
(c) => c.DownloadsComponent
|
||||
),
|
||||
},
|
||||
...withWorkspaceLayout(xtreamRoutes),
|
||||
...withWorkspaceLayout(stalkerRoutes),
|
||||
...withWorkspaceLayout(
|
||||
createXtreamRoutes(xtreamFeatureRouteOptions)
|
||||
),
|
||||
...withWorkspaceLayout(
|
||||
createStalkerRoutes(stalkerFeatureRouteOptions)
|
||||
),
|
||||
{
|
||||
path: 'settings',
|
||||
data: { layout: 'workspace' },
|
||||
|
||||
+4
-4
@@ -1,10 +1,10 @@
|
||||
@if (selectedCategory()) {
|
||||
@if (isStalker && selectedItem()) {
|
||||
@if (isStalker && selectedStalkerItem()) {
|
||||
@if (contentType() === 'vod') {
|
||||
@if (selectedItem().series?.length > 0) {
|
||||
@if (selectedStalkerItem().series?.length > 0) {
|
||||
<!-- VOD item with embedded series (vclub or Ministra) -->
|
||||
<app-stalker-series-view [vodWithSeries]="selectedItem()" />
|
||||
} @else if (selectedItem().is_series) {
|
||||
<app-stalker-series-view [vodWithSeries]="selectedStalkerItem()" />
|
||||
} @else if (selectedStalkerItem().is_series) {
|
||||
<!-- VOD item that is actually a series (Ministra plugin is_series=1) -->
|
||||
<app-stalker-series-view />
|
||||
} @else {
|
||||
|
||||
+128
-254
@@ -14,7 +14,6 @@ import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { PlaylistsService } from 'services';
|
||||
import {
|
||||
@@ -24,31 +23,32 @@ import {
|
||||
StalkerVodDetails,
|
||||
VodDetailsItem,
|
||||
} from 'shared-interfaces';
|
||||
import { DownloadsService } from '../../services/downloads.service';
|
||||
import { ExternalPlaybackService } from '../../services/external-playback.service';
|
||||
import { PlayerService } from '../../services/player.service';
|
||||
import { GridListComponent } from '../../shared/components/grid-list/grid-list.component';
|
||||
import { PlaylistErrorViewComponent } from '@iptvnator/portal/shared/ui';
|
||||
import { createLogger } from '@iptvnator/portal/shared/util';
|
||||
import { StalkerVodSource } from '@iptvnator/portal/stalker/data-access';
|
||||
import { StalkerSeriesViewComponent } from '../../stalker/stalker-series-view/stalker-series-view.component';
|
||||
import {
|
||||
buildStalkerSelectedVodItem,
|
||||
createLogger,
|
||||
isStalkerPortalCatalogFacade,
|
||||
PortalCatalogFacade,
|
||||
PortalCatalogSortMode,
|
||||
PORTAL_CATALOG_FACADE,
|
||||
StalkerPortalCatalogFacade,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import {
|
||||
createPortalFavoritesResource,
|
||||
createRefreshTrigger,
|
||||
isSelectedStalkerVodFavorite,
|
||||
normalizeStalkerEntityId,
|
||||
normalizeStalkerEntityIdAsNumber,
|
||||
StalkerSelectedVodItem,
|
||||
toggleStalkerVodFavorite,
|
||||
} from '@iptvnator/portal/stalker/data-access';
|
||||
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
|
||||
import { XTREAM_DATA_SOURCE } from '@iptvnator/portal/xtream/data-access';
|
||||
import { XtreamCategorySortMode } from '@iptvnator/portal/xtream/data-access';
|
||||
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
|
||||
import { DownloadsService } from '../../services/downloads.service';
|
||||
import { ExternalPlaybackService } from '../../services/external-playback.service';
|
||||
import { PlayerService } from '../../services/player.service';
|
||||
import { GridListComponent } from '../../shared/components/grid-list/grid-list.component';
|
||||
import { StalkerSeriesViewComponent } from '../../stalker/stalker-series-view/stalker-series-view.component';
|
||||
import { VodDetailsComponent } from '../vod-details/vod-details.component';
|
||||
|
||||
const SORT_STORAGE_KEY = 'xtream-category-sort-mode';
|
||||
|
||||
interface CategoryContentItem {
|
||||
id?: number | string;
|
||||
is_series?: number | string | boolean;
|
||||
@@ -59,17 +59,6 @@ interface CategoryContentItem {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface CategoryStoreLike {
|
||||
totalCount?: () => number;
|
||||
selectItemsFromSelectedCategory?: () => unknown[];
|
||||
getProgressPercent?: (id: number, type: 'vod') => number;
|
||||
hasSeriesProgress?: (id: number) => boolean;
|
||||
isWatched?: (id: number, type: 'vod') => boolean;
|
||||
currentPlaylist?: () => { id?: string } | null;
|
||||
loadAllPositions?: (playlistId: string) => void;
|
||||
selectedCategoryId?: () => number | null;
|
||||
}
|
||||
|
||||
interface DownloadVodData {
|
||||
id?: string | number;
|
||||
has_files?: unknown;
|
||||
@@ -105,62 +94,47 @@ export class CategoryContentViewComponent implements OnInit, OnDestroy {
|
||||
readonly externalPlayback = inject(ExternalPlaybackService);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly translateService = inject(TranslateService);
|
||||
|
||||
readonly isStalker = this.activatedRoute.snapshot.data['api'] === 'stalker';
|
||||
private readonly playlistService = inject(PlaylistsService);
|
||||
private readonly downloadsService = inject(DownloadsService);
|
||||
private readonly catalog = inject(
|
||||
PORTAL_CATALOG_FACADE
|
||||
) as PortalCatalogFacade<
|
||||
CategoryContentItem,
|
||||
CategoryContentItem,
|
||||
CategoryContentItem
|
||||
>;
|
||||
private readonly stalkerCatalog:
|
||||
| StalkerPortalCatalogFacade<
|
||||
CategoryContentItem,
|
||||
CategoryContentItem,
|
||||
CategoryContentItem
|
||||
>
|
||||
| null = isStalkerPortalCatalogFacade(this.catalog)
|
||||
? (this.catalog as StalkerPortalCatalogFacade<
|
||||
CategoryContentItem,
|
||||
CategoryContentItem,
|
||||
CategoryContentItem
|
||||
>)
|
||||
: null;
|
||||
private readonly logger = createLogger('CategoryContentView');
|
||||
private readonly store = this.isStalker
|
||||
? inject(StalkerStore)
|
||||
: inject(XtreamStore);
|
||||
private readonly favoritesRefresh = createRefreshTrigger();
|
||||
|
||||
readonly isStalker = this.catalog.provider === 'stalker';
|
||||
|
||||
/** Unsubscribe function for playback position updates */
|
||||
private unsubscribePositionUpdates: (() => void) | null = null;
|
||||
|
||||
readonly contentType = this.store.selectedContentType;
|
||||
private readonly playlistService = inject(PlaylistsService);
|
||||
private readonly downloadsService = inject(DownloadsService);
|
||||
private readonly favoritesRefresh = createRefreshTrigger();
|
||||
readonly contentType = this.catalog.contentType;
|
||||
readonly inlinePlayback = signal<ResolvedPortalPlayback | null>(null);
|
||||
private lastInlineSaveTime = 0;
|
||||
|
||||
readonly limit = this.store.limit;
|
||||
readonly pageIndex = this.store.page;
|
||||
readonly pageSizeOptions = this.isStalker ? [14] : [10, 25, 50, 100];
|
||||
readonly selectedCategory = this.store.getSelectedCategory;
|
||||
readonly paginatedContent = this.store.getPaginatedContent;
|
||||
readonly selectedCategoryTitle = computed(() => {
|
||||
const category = this.selectedCategory();
|
||||
if (!category) return '';
|
||||
|
||||
// category_name is the StalkerCategoryItem field; name is the Xtream field
|
||||
const fromCategory =
|
||||
category.category_name ?? category.name ?? category.title ?? '';
|
||||
if (fromCategory) return fromCategory;
|
||||
|
||||
// Stalker: fall back to getSelectedCategoryName computed (looks up directly from categories array)
|
||||
if (this.isStalker) {
|
||||
const stalkerStore = this.store as unknown as {
|
||||
getSelectedCategoryName?: () => string;
|
||||
};
|
||||
const fromStoreComputed =
|
||||
stalkerStore.getSelectedCategoryName?.() ?? '';
|
||||
if (fromStoreComputed) return fromStoreComputed;
|
||||
}
|
||||
|
||||
return '';
|
||||
});
|
||||
readonly categoryItemCount = computed(() => {
|
||||
const storeLike = this.store as unknown as CategoryStoreLike;
|
||||
|
||||
if (this.isStalker && typeof storeLike.totalCount === 'function') {
|
||||
return storeLike.totalCount();
|
||||
}
|
||||
|
||||
if (typeof storeLike.selectItemsFromSelectedCategory === 'function') {
|
||||
return storeLike.selectItemsFromSelectedCategory().length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
});
|
||||
readonly limit = this.catalog.limit;
|
||||
readonly pageIndex = this.catalog.pageIndex;
|
||||
readonly pageSizeOptions = Array.from(this.catalog.pageSizeOptions);
|
||||
readonly selectedCategory = this.catalog.selectedCategory;
|
||||
readonly paginatedContent = this.catalog.paginatedContent;
|
||||
readonly selectedCategoryTitle = this.catalog.selectedCategoryTitle;
|
||||
readonly categoryItemCount = this.catalog.categoryItemCount;
|
||||
readonly categoryItemSubtitle = computed(() => {
|
||||
const itemCount = this.categoryItemCount();
|
||||
return `${itemCount} ${itemCount === 1 ? 'item' : 'items'}`;
|
||||
@@ -178,14 +152,9 @@ export class CategoryContentViewComponent implements OnInit, OnDestroy {
|
||||
private loadedPositionsForPlaylistId: string | null = null;
|
||||
|
||||
constructor() {
|
||||
// Effect to load Stalker positions when playlist becomes available
|
||||
if (this.isStalker) {
|
||||
effect(() => {
|
||||
const stalkerStore = this.store as InstanceType<
|
||||
typeof StalkerStore
|
||||
>;
|
||||
const playlist = stalkerStore.currentPlaylist();
|
||||
const playlistId = playlist?._id;
|
||||
const playlistId = this.catalog.playlist()?.id;
|
||||
|
||||
if (
|
||||
playlistId &&
|
||||
@@ -196,20 +165,15 @@ export class CategoryContentViewComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
});
|
||||
|
||||
// Effect to load position for selected VOD item (ensures fresh data after playback)
|
||||
effect(() => {
|
||||
const stalkerStore = this.store as InstanceType<
|
||||
typeof StalkerStore
|
||||
>;
|
||||
const item = this.selectedItem();
|
||||
const playlist = stalkerStore.currentPlaylist();
|
||||
const playlistId = this.catalog.playlist()?.id;
|
||||
|
||||
if (item && playlist?._id && this.contentType() === 'vod') {
|
||||
this.loadSingleVodPosition(playlist._id, Number(item.id));
|
||||
if (item && playlistId && this.contentType() === 'vod') {
|
||||
this.loadSingleVodPosition(playlistId, Number(item.id));
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for real-time playback position updates from video player
|
||||
if (window.electron?.onPlaybackPositionUpdate) {
|
||||
this.unsubscribePositionUpdates =
|
||||
window.electron.onPlaybackPositionUpdate(
|
||||
@@ -237,25 +201,19 @@ export class CategoryContentViewComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
readonly contentWithProgress = computed(() => {
|
||||
const items = this.store.getPaginatedContent();
|
||||
const items = this.paginatedContent();
|
||||
if (!items) return [];
|
||||
|
||||
if (this.isStalker) {
|
||||
// Enrich Stalker items with progress data
|
||||
const positions = this.stalkerPositions();
|
||||
const seriesPositions = this.stalkerSeriesPositions();
|
||||
const contentType = this.contentType();
|
||||
|
||||
return items.map((item: CategoryContentItem) => {
|
||||
const id = Number(item.id);
|
||||
|
||||
// Check if this item has series/episode progress (watched episodes)
|
||||
// This works even when is_series flag isn't set on the item
|
||||
const hasSeriesProgress = Boolean(
|
||||
seriesPositions.get(id)?.length
|
||||
);
|
||||
|
||||
// For series content type, or items with is_series flag
|
||||
const isSeries =
|
||||
contentType === 'series' ||
|
||||
item.is_series === '1' ||
|
||||
@@ -263,73 +221,59 @@ export class CategoryContentViewComponent implements OnInit, OnDestroy {
|
||||
|
||||
if (hasSeriesProgress) {
|
||||
return { ...item, hasSeriesProgress: true };
|
||||
} else if (isSeries) {
|
||||
return { ...item, hasSeriesProgress: false };
|
||||
} else {
|
||||
// Regular VOD item
|
||||
const vodKey = `vod_${id}`;
|
||||
const vodPosition = positions.get(vodKey);
|
||||
const vodProgress = this.calculateProgress(vodPosition);
|
||||
return {
|
||||
...item,
|
||||
progress: vodProgress,
|
||||
isWatched: vodProgress >= 90,
|
||||
};
|
||||
}
|
||||
|
||||
if (isSeries) {
|
||||
return { ...item, hasSeriesProgress: false };
|
||||
}
|
||||
|
||||
const vodKey = `vod_${id}`;
|
||||
const vodPosition = positions.get(vodKey);
|
||||
const vodProgress = this.calculateProgress(vodPosition);
|
||||
return {
|
||||
...item,
|
||||
progress: vodProgress,
|
||||
isWatched: vodProgress >= 90,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const xtreamStore = this.store as unknown as CategoryStoreLike;
|
||||
if (!xtreamStore.getProgressPercent) return items;
|
||||
|
||||
return items.map((item: CategoryContentItem) => {
|
||||
const isSeries = this.contentType() === 'series';
|
||||
const id = Number(
|
||||
item.xtream_id || item.series_id || item.stream_id
|
||||
);
|
||||
|
||||
if (isSeries) {
|
||||
return {
|
||||
...item,
|
||||
hasSeriesProgress: xtreamStore.hasSeriesProgress(id),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...item,
|
||||
progress: xtreamStore.getProgressPercent(id, 'vod'),
|
||||
isWatched: xtreamStore.isWatched(id, 'vod'),
|
||||
};
|
||||
}
|
||||
});
|
||||
return items.map((item: CategoryContentItem) => ({
|
||||
...item,
|
||||
...this.catalog.getItemProgress(item),
|
||||
}));
|
||||
});
|
||||
|
||||
readonly isPaginatedContentLoading = this.store.isPaginatedContentLoading;
|
||||
readonly selectedItem = this.store.selectedItem;
|
||||
readonly totalPages = this.store.getTotalPages;
|
||||
readonly bigStore = inject(Store);
|
||||
readonly contentSortMode = !this.isStalker
|
||||
? (this.store as unknown as InstanceType<typeof XtreamStore>)
|
||||
.contentSortMode
|
||||
: signal<XtreamCategorySortMode>('date-desc');
|
||||
readonly isPaginatedContentLoading =
|
||||
this.catalog.isPaginatedContentLoading;
|
||||
readonly selectedItem = this.catalog.selectedItem;
|
||||
readonly totalPages = this.catalog.totalPages;
|
||||
readonly contentSortMode = this.catalog.contentSortMode;
|
||||
readonly selectedStalkerItem = computed<StalkerSelectedVodItem | null>(
|
||||
() => {
|
||||
if (!this.isStalker) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (this.selectedItem() as unknown as StalkerSelectedVodItem | null) ?? null;
|
||||
}
|
||||
);
|
||||
|
||||
/** Computed VodDetailsItem for the vod-details component */
|
||||
readonly vodDetailsItem = computed<VodDetailsItem | null>(() => {
|
||||
const item = this.selectedItem();
|
||||
const item = this.selectedStalkerItem();
|
||||
if (!item || !this.isStalker) return null;
|
||||
// Access currentPlaylist from the store (type-safe since we're in stalker mode)
|
||||
const stalkerStore = this.store as unknown as {
|
||||
currentPlaylist: () => { _id: string } | null;
|
||||
};
|
||||
|
||||
return createStalkerVodItem(
|
||||
item as StalkerVodDetails,
|
||||
stalkerStore.currentPlaylist()?._id ?? ''
|
||||
item as unknown as StalkerVodDetails,
|
||||
this.catalog.playlist()?.id ?? ''
|
||||
);
|
||||
});
|
||||
|
||||
/** Playback position for the selected VOD item (Stalker) */
|
||||
readonly selectedVodPlaybackPosition = computed<number | null>(() => {
|
||||
if (!this.isStalker) return null;
|
||||
const item = this.selectedItem();
|
||||
const item = this.selectedStalkerItem();
|
||||
if (!item) return null;
|
||||
|
||||
const id = Number(item.id);
|
||||
@@ -341,13 +285,7 @@ export class CategoryContentViewComponent implements OnInit, OnDestroy {
|
||||
|
||||
readonly portalFavorites = createPortalFavoritesResource(
|
||||
this.playlistService,
|
||||
() => {
|
||||
if (!this.isStalker) return undefined;
|
||||
const stalkerStore = this.store as InstanceType<
|
||||
typeof StalkerStore
|
||||
>;
|
||||
return stalkerStore.currentPlaylist()?._id;
|
||||
},
|
||||
() => (this.isStalker ? this.catalog.playlist()?.id : undefined),
|
||||
() => this.favoritesRefresh.refreshVersion()
|
||||
);
|
||||
|
||||
@@ -360,60 +298,23 @@ export class CategoryContentViewComponent implements OnInit, OnDestroy {
|
||||
|
||||
seasons = [];
|
||||
|
||||
setContentSortMode(mode: XtreamCategorySortMode): void {
|
||||
(
|
||||
this.store as unknown as InstanceType<typeof XtreamStore>
|
||||
).setContentSortMode(mode);
|
||||
localStorage.setItem(SORT_STORAGE_KEY, mode);
|
||||
setContentSortMode(mode: PortalCatalogSortMode): void {
|
||||
this.catalog.setContentSortMode(mode);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
const { categoryId } = this.activatedRoute.snapshot.params;
|
||||
|
||||
// Ensure playback positions are loaded (Xtream only - Stalker is handled via effect in constructor)
|
||||
if (!this.isStalker) {
|
||||
const savedSortMode = localStorage.getItem(SORT_STORAGE_KEY);
|
||||
if (
|
||||
savedSortMode === 'date-desc' ||
|
||||
savedSortMode === 'date-asc' ||
|
||||
savedSortMode === 'name-asc' ||
|
||||
savedSortMode === 'name-desc'
|
||||
) {
|
||||
(
|
||||
this.store as unknown as InstanceType<typeof XtreamStore>
|
||||
).setContentSortMode(savedSortMode);
|
||||
}
|
||||
const xtreamStore = this.store as unknown as CategoryStoreLike;
|
||||
const playlistId = xtreamStore.currentPlaylist?.()?.id;
|
||||
if (playlistId) {
|
||||
xtreamStore.loadAllPositions?.(playlistId);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear any previous selectedItem when entering category view
|
||||
// This ensures the content-header is visible
|
||||
this.store.setSelectedItem(null);
|
||||
|
||||
// Set category - setSelectedCategory only resets page if category actually changes
|
||||
// This preserves the page state when navigating back from detail view
|
||||
if (categoryId) {
|
||||
this.store.setSelectedCategory(categoryId);
|
||||
} else {
|
||||
// Stalker uses "*" as the "All" category id
|
||||
if (this.isStalker) {
|
||||
(
|
||||
this.store as InstanceType<typeof StalkerStore>
|
||||
).setSelectedCategory('*');
|
||||
} else {
|
||||
this.store.setSelectedCategory(null);
|
||||
}
|
||||
}
|
||||
this.catalog.initialize(categoryId ?? null);
|
||||
|
||||
if (this.isStalker) {
|
||||
const storeLike = this.store as unknown as CategoryStoreLike;
|
||||
const selectedCategory =
|
||||
(this.selectedCategory() as CategoryContentItem | null) ?? null;
|
||||
this.logger.warn('Stalker category init from route', {
|
||||
routeCategoryId: categoryId ?? null,
|
||||
selectedCategoryId: storeLike.selectedCategoryId?.(),
|
||||
selectedCategoryId:
|
||||
selectedCategory?.category_id ??
|
||||
selectedCategory?.id ??
|
||||
null,
|
||||
selectedContentType: this.contentType(),
|
||||
});
|
||||
}
|
||||
@@ -433,7 +334,6 @@ export class CategoryContentViewComponent implements OnInit, OnDestroy {
|
||||
const key = `${pos.contentType}_${pos.contentXtreamId}`;
|
||||
positionsMap.set(key, pos);
|
||||
|
||||
// Group episodes by series ID
|
||||
if (pos.contentType === 'episode' && pos.seriesXtreamId) {
|
||||
const existing = seriesMap.get(pos.seriesXtreamId) || [];
|
||||
existing.push(pos);
|
||||
@@ -477,7 +377,6 @@ export class CategoryContentViewComponent implements OnInit, OnDestroy {
|
||||
const percent =
|
||||
(position.positionSeconds / position.durationSeconds) * 100;
|
||||
|
||||
// If watched > 10s but percent < 1, return 1 to show visual progress
|
||||
if (position.positionSeconds > 10 && percent < 1) {
|
||||
return 1;
|
||||
}
|
||||
@@ -486,36 +385,16 @@ export class CategoryContentViewComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent) {
|
||||
this.store.setPage(event.pageIndex);
|
||||
this.store.setLimit(event.pageSize);
|
||||
localStorage.setItem('xtream-page-size', event.pageSize.toString());
|
||||
this.catalog.setPage(event.pageIndex);
|
||||
this.catalog.setLimit(event.pageSize);
|
||||
}
|
||||
|
||||
onItemClick(item: CategoryContentItem) {
|
||||
const selectedItem = buildStalkerSelectedVodItem(
|
||||
item as StalkerVodSource,
|
||||
this.contentType() === 'vod' &&
|
||||
(item.is_series === '1' || item.is_series === 1)
|
||||
);
|
||||
|
||||
if (this.isStalker) {
|
||||
this.store.setSelectedItem(selectedItem);
|
||||
} else {
|
||||
const xtreamStore = this.store as unknown as CategoryStoreLike;
|
||||
// When viewing "Recently Added" (no category selected), include category_id in path
|
||||
const categoryId = xtreamStore.selectedCategoryId?.();
|
||||
if (categoryId && item.xtream_id !== undefined) {
|
||||
this.router.navigate([item.xtream_id], {
|
||||
relativeTo: this.activatedRoute,
|
||||
});
|
||||
} else if (
|
||||
item.category_id !== undefined &&
|
||||
item.xtream_id !== undefined
|
||||
) {
|
||||
this.router.navigate([item.category_id, item.xtream_id], {
|
||||
relativeTo: this.activatedRoute,
|
||||
});
|
||||
}
|
||||
const navigation = this.catalog.selectItem(item);
|
||||
if (navigation?.length) {
|
||||
this.router.navigate(navigation, {
|
||||
relativeTo: this.activatedRoute,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -524,31 +403,30 @@ export class CategoryContentViewComponent implements OnInit, OnDestroy {
|
||||
title?: string,
|
||||
thumbnail?: string
|
||||
) {
|
||||
await this.store.createLinkToPlayVod(cmd, title, thumbnail);
|
||||
if (!this.stalkerCatalog) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.stalkerCatalog.createLinkToPlayVod(cmd, title, thumbnail);
|
||||
}
|
||||
|
||||
addToFavorites(item: Record<string, unknown>, onDone?: () => void) {
|
||||
this.logger.debug('Add to favorites', item);
|
||||
if (this.isStalker) {
|
||||
(this.store as InstanceType<typeof StalkerStore>).addToFavorites(
|
||||
item,
|
||||
onDone
|
||||
);
|
||||
if (this.stalkerCatalog) {
|
||||
this.stalkerCatalog.addToFavorites(item, onDone);
|
||||
return;
|
||||
}
|
||||
this.store.addToFavorites(item);
|
||||
|
||||
onDone?.();
|
||||
}
|
||||
|
||||
removeFromFavorites(favoriteId: string, onDone?: () => void) {
|
||||
this.logger.debug('Remove from favorites', favoriteId);
|
||||
if (this.isStalker) {
|
||||
(
|
||||
this.store as InstanceType<typeof StalkerStore>
|
||||
).removeFromFavorites(favoriteId, onDone);
|
||||
if (this.stalkerCatalog) {
|
||||
this.stalkerCatalog.removeFromFavorites(favoriteId, onDone);
|
||||
return;
|
||||
}
|
||||
this.store.removeFromFavorites(favoriteId);
|
||||
|
||||
onDone?.();
|
||||
}
|
||||
|
||||
@@ -596,7 +474,7 @@ export class CategoryContentViewComponent implements OnInit, OnDestroy {
|
||||
/** Handle back from vod-details component */
|
||||
onVodBack(): void {
|
||||
this.closeInlinePlayer();
|
||||
this.store.setSelectedItem(null);
|
||||
this.catalog.clearSelectedItem();
|
||||
}
|
||||
|
||||
handleInlineTimeUpdate(event: {
|
||||
@@ -644,17 +522,15 @@ export class CategoryContentViewComponent implements OnInit, OnDestroy {
|
||||
|
||||
/** Handle download from vod-details component */
|
||||
async onVodDownload(item: VodDetailsItem): Promise<void> {
|
||||
if (item.type !== 'stalker') return;
|
||||
if (item.type !== 'stalker' || !this.stalkerCatalog) return;
|
||||
|
||||
const stalkerStore = this.store as InstanceType<typeof StalkerStore>;
|
||||
const playlist = stalkerStore.currentPlaylist();
|
||||
const playlist = this.catalog.playlist();
|
||||
if (!playlist || !playlist.portalUrl || !playlist.macAddress) return;
|
||||
|
||||
let cmdToUse = item.cmd;
|
||||
const itemData = item.data as DownloadVodData;
|
||||
const normalizedItemId = normalizeStalkerEntityId(itemData?.id);
|
||||
|
||||
// Align with playback path: resolve has_files references to /media/file_{id}.mpg
|
||||
if (
|
||||
itemData?.has_files !== undefined &&
|
||||
cmdToUse &&
|
||||
@@ -663,13 +539,13 @@ export class CategoryContentViewComponent implements OnInit, OnDestroy {
|
||||
!cmdToUse.includes('/media/file_')
|
||||
) {
|
||||
const fileId =
|
||||
await stalkerStore.fetchMovieFileId(normalizedItemId);
|
||||
await this.stalkerCatalog.fetchMovieFileId(normalizedItemId);
|
||||
if (fileId) {
|
||||
cmdToUse = `/media/file_${fileId}.mpg`;
|
||||
}
|
||||
}
|
||||
|
||||
const url = await stalkerStore.fetchLinkToPlay(
|
||||
const url = await this.stalkerCatalog.fetchLinkToPlay(
|
||||
playlist.portalUrl,
|
||||
playlist.macAddress,
|
||||
cmdToUse
|
||||
@@ -679,7 +555,7 @@ export class CategoryContentViewComponent implements OnInit, OnDestroy {
|
||||
const numericId = normalizeStalkerEntityIdAsNumber(itemData?.id) ?? 0;
|
||||
|
||||
await this.downloadsService.startDownload({
|
||||
playlistId: playlist._id,
|
||||
playlistId: playlist.id,
|
||||
xtreamId: numericId,
|
||||
contentType: 'vod',
|
||||
title: itemData?.info?.name || itemData?.title || 'Unknown',
|
||||
@@ -687,10 +563,9 @@ export class CategoryContentViewComponent implements OnInit, OnDestroy {
|
||||
posterUrl: itemData?.info?.movie_image,
|
||||
headers: {
|
||||
userAgent: playlist.userAgent,
|
||||
referer: playlist.referrer,
|
||||
referer: playlist.referer,
|
||||
origin: playlist.origin,
|
||||
},
|
||||
// Playlist info for auto-creation (Stalker playlists)
|
||||
playlistName: playlist.title || 'Stalker Portal',
|
||||
playlistType: 'stalker',
|
||||
portalUrl: playlist.portalUrl,
|
||||
@@ -709,16 +584,15 @@ export class CategoryContentViewComponent implements OnInit, OnDestroy {
|
||||
thumbnail?: string,
|
||||
startTime?: number
|
||||
): Promise<void> {
|
||||
if (!this.stalkerCatalog) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const stalkerStore = this.store as InstanceType<
|
||||
typeof StalkerStore
|
||||
>;
|
||||
const playback = await stalkerStore.resolveVodPlayback(
|
||||
const playback = await this.stalkerCatalog.resolveVodPlayback(
|
||||
cmd,
|
||||
title,
|
||||
thumbnail,
|
||||
undefined,
|
||||
undefined,
|
||||
startTime
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import { Provider, Injectable, computed, inject } from '@angular/core';
|
||||
import {
|
||||
buildStalkerSelectedVodItem,
|
||||
StalkerStore,
|
||||
StalkerVodSource,
|
||||
} from '@iptvnator/portal/stalker/data-access';
|
||||
import {
|
||||
PortalCatalogItemProgress,
|
||||
PortalCatalogPlaylistMeta,
|
||||
PortalCatalogSortMode,
|
||||
PORTAL_CATALOG_FACADE,
|
||||
StalkerPortalCatalogFacade,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
|
||||
@Injectable()
|
||||
export class StalkerCatalogFacadeService
|
||||
implements
|
||||
StalkerPortalCatalogFacade<
|
||||
Record<string, unknown>,
|
||||
StalkerVodSource,
|
||||
StalkerVodSource
|
||||
>
|
||||
{
|
||||
private readonly stalkerStore = inject(StalkerStore);
|
||||
|
||||
readonly provider = 'stalker' as const;
|
||||
readonly pageSizeOptions = [14] as const;
|
||||
readonly contentType = this.stalkerStore.selectedContentType;
|
||||
readonly limit = this.stalkerStore.limit;
|
||||
readonly pageIndex = this.stalkerStore.page;
|
||||
readonly selectedCategory = this.stalkerStore.getSelectedCategory;
|
||||
readonly paginatedContent = computed(
|
||||
() => this.stalkerStore.getPaginatedContent() ?? []
|
||||
);
|
||||
readonly selectedItem = this.stalkerStore.selectedItem;
|
||||
readonly totalPages = this.stalkerStore.getTotalPages;
|
||||
readonly isPaginatedContentLoading =
|
||||
this.stalkerStore.isPaginatedContentLoading;
|
||||
readonly selectedCategoryTitle = computed(() => {
|
||||
const category = this.selectedCategory();
|
||||
const fromCategory = String(
|
||||
category?.['category_name'] ?? category?.['name'] ?? ''
|
||||
);
|
||||
|
||||
if (fromCategory) {
|
||||
return fromCategory;
|
||||
}
|
||||
|
||||
return this.stalkerStore.getSelectedCategoryName() ?? '';
|
||||
});
|
||||
readonly categoryItemCount = computed(() => this.stalkerStore.totalCount());
|
||||
readonly contentSortMode = computed<PortalCatalogSortMode | null>(
|
||||
() => null
|
||||
);
|
||||
readonly playlist = computed<PortalCatalogPlaylistMeta | null>(() => {
|
||||
const playlist = this.stalkerStore.currentPlaylist();
|
||||
if (!playlist?._id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: playlist._id,
|
||||
title: playlist.title ?? 'Stalker Portal',
|
||||
portalUrl: playlist.portalUrl,
|
||||
macAddress: playlist.macAddress,
|
||||
userAgent: playlist.userAgent,
|
||||
referer: playlist.referrer,
|
||||
origin: playlist.origin,
|
||||
};
|
||||
});
|
||||
|
||||
initialize(categoryId?: string | null): void {
|
||||
this.clearSelectedItem();
|
||||
if (categoryId) {
|
||||
this.stalkerStore.setSelectedCategory(categoryId);
|
||||
return;
|
||||
}
|
||||
|
||||
this.stalkerStore.setSelectedCategory('*');
|
||||
}
|
||||
|
||||
clearSelectedItem(): void {
|
||||
this.stalkerStore.clearSelectedItem();
|
||||
}
|
||||
|
||||
setPage(page: number): void {
|
||||
this.stalkerStore.setPage(page);
|
||||
}
|
||||
|
||||
setLimit(limit: number): void {
|
||||
this.stalkerStore.setLimit(limit);
|
||||
}
|
||||
|
||||
setContentSortMode(_mode: PortalCatalogSortMode): void {
|
||||
// Stalker catalog content is server-paginated and does not support local sort modes.
|
||||
}
|
||||
|
||||
selectItem(item: StalkerVodSource): string[] | null {
|
||||
const needsSeriesFetch =
|
||||
this.contentType() === 'vod' &&
|
||||
(item.is_series === '1' || item.is_series === 1);
|
||||
|
||||
this.stalkerStore.setSelectedItem(
|
||||
buildStalkerSelectedVodItem(item, needsSeriesFetch)
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
getItemProgress(
|
||||
_item: StalkerVodSource
|
||||
): PortalCatalogItemProgress {
|
||||
return {};
|
||||
}
|
||||
|
||||
async createLinkToPlayVod(
|
||||
cmd?: string,
|
||||
title?: string,
|
||||
thumbnail?: string
|
||||
): Promise<void> {
|
||||
await this.stalkerStore.createLinkToPlayVod(cmd, title, thumbnail);
|
||||
}
|
||||
|
||||
addToFavorites(item: Record<string, unknown>, onDone?: () => void): void {
|
||||
this.stalkerStore.addToFavorites(item, onDone);
|
||||
}
|
||||
|
||||
removeFromFavorites(favoriteId: string, onDone?: () => void): void {
|
||||
this.stalkerStore.removeFromFavorites(favoriteId, onDone);
|
||||
}
|
||||
|
||||
fetchMovieFileId(itemId: string): Promise<string | null> {
|
||||
return this.stalkerStore.fetchMovieFileId(itemId);
|
||||
}
|
||||
|
||||
async fetchLinkToPlay(
|
||||
portalUrl: string,
|
||||
macAddress: string,
|
||||
cmd: string
|
||||
): Promise<string> {
|
||||
return this.stalkerStore.fetchLinkToPlay(portalUrl, macAddress, cmd);
|
||||
}
|
||||
|
||||
resolveVodPlayback(
|
||||
cmd?: string,
|
||||
title?: string,
|
||||
thumbnail?: string,
|
||||
startTime?: number
|
||||
) {
|
||||
return this.stalkerStore.resolveVodPlayback(
|
||||
cmd,
|
||||
title,
|
||||
thumbnail,
|
||||
undefined,
|
||||
undefined,
|
||||
startTime
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function provideStalkerCatalogFacade(): Provider[] {
|
||||
return [
|
||||
StalkerCatalogFacadeService,
|
||||
{
|
||||
provide: PORTAL_CATALOG_FACADE,
|
||||
useExisting: StalkerCatalogFacadeService,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { StalkerFeatureRouteOptions } from '@iptvnator/portal/stalker/feature';
|
||||
import { provideStalkerCatalogFacade } from './stalker-catalog-facade.service';
|
||||
|
||||
// Temporary app-side loader map while Stalker route screens remain in apps/web.
|
||||
export const stalkerFeatureRouteOptions: StalkerFeatureRouteOptions = {
|
||||
catalogProviders: provideStalkerCatalogFacade(),
|
||||
loadShellComponent: () =>
|
||||
import('./stalker-shell/stalker-shell.component').then(
|
||||
(c) => c.StalkerShellComponent
|
||||
),
|
||||
loadMainContainerComponent: () =>
|
||||
import('./stalker-main-container.component').then(
|
||||
(c) => c.StalkerMainContainerComponent
|
||||
),
|
||||
loadCategoryContentViewComponent: () =>
|
||||
import(
|
||||
'../portal-shared/category-content-view/category-content-view.component'
|
||||
).then((c) => c.CategoryContentViewComponent),
|
||||
loadLiveStreamLayoutComponent: () =>
|
||||
import(
|
||||
'./stalker-live-stream-layout/stalker-live-stream-layout.component'
|
||||
).then((c) => c.StalkerLiveStreamLayoutComponent),
|
||||
loadFavoritesComponent: () =>
|
||||
import('./stalker-favorites/stalker-favorites.component').then(
|
||||
(c) => c.StalkerFavoritesComponent
|
||||
),
|
||||
loadRecentlyViewedComponent: () =>
|
||||
import('./recently-viewed/recently-viewed.component').then(
|
||||
(c) => c.RecentlyViewedComponent
|
||||
),
|
||||
loadSearchComponent: () =>
|
||||
import('./stalker-search/stalker-search.component').then(
|
||||
(c) => c.StalkerSearchComponent
|
||||
),
|
||||
};
|
||||
@@ -1,121 +0,0 @@
|
||||
import { Route } from '@angular/router';
|
||||
|
||||
export const stalkerRoutes: Route[] = [
|
||||
{
|
||||
path: 'stalker/:id',
|
||||
loadComponent: () =>
|
||||
import('./stalker-shell/stalker-shell.component').then(
|
||||
(c) => c.StalkerShellComponent
|
||||
),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'vod',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'vod',
|
||||
loadComponent: () =>
|
||||
import('./stalker-main-container.component').then(
|
||||
(c) => c.StalkerMainContainerComponent
|
||||
),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
data: {
|
||||
api: 'stalker',
|
||||
contentType: 'vod',
|
||||
},
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'../portal-shared/category-content-view/category-content-view.component'
|
||||
).then((c) => c.CategoryContentViewComponent),
|
||||
},
|
||||
{
|
||||
path: ':categoryId',
|
||||
data: {
|
||||
api: 'stalker',
|
||||
contentType: 'vod',
|
||||
},
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'../portal-shared/category-content-view/category-content-view.component'
|
||||
).then((c) => c.CategoryContentViewComponent),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'vod',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'itv',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'./stalker-live-stream-layout/stalker-live-stream-layout.component'
|
||||
).then((c) => c.StalkerLiveStreamLayoutComponent),
|
||||
},
|
||||
{
|
||||
path: 'series',
|
||||
loadComponent: () =>
|
||||
import('./stalker-main-container.component').then(
|
||||
(c) => c.StalkerMainContainerComponent
|
||||
),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'../portal-shared/category-content-view/category-content-view.component'
|
||||
).then((c) => c.CategoryContentViewComponent),
|
||||
data: {
|
||||
api: 'stalker',
|
||||
contentType: 'series',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: ':categoryId',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'../portal-shared/category-content-view/category-content-view.component'
|
||||
).then((c) => c.CategoryContentViewComponent),
|
||||
data: {
|
||||
api: 'stalker',
|
||||
contentType: 'series',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'favorites',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'./stalker-favorites/stalker-favorites.component'
|
||||
).then((c) => c.StalkerFavoritesComponent),
|
||||
},
|
||||
{
|
||||
path: 'recent',
|
||||
loadComponent: () =>
|
||||
import('./recently-viewed/recently-viewed.component').then(
|
||||
(c) => c.RecentlyViewedComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
loadComponent: () =>
|
||||
import('./stalker-search/stalker-search.component').then(
|
||||
(c) => c.StalkerSearchComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'downloads',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'@iptvnator/portal/downloads/feature'
|
||||
).then((c) => c.DownloadsComponent),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,144 @@
|
||||
import { Provider, Injectable, computed, inject } from '@angular/core';
|
||||
import {
|
||||
PortalCatalogFacade,
|
||||
PortalCatalogItemProgress,
|
||||
PortalCatalogPlaylistMeta,
|
||||
PortalCatalogSortMode,
|
||||
PORTAL_CATALOG_FACADE,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
|
||||
|
||||
const SORT_STORAGE_KEY = 'xtream-category-sort-mode';
|
||||
|
||||
@Injectable()
|
||||
export class XtreamCatalogFacadeService
|
||||
implements PortalCatalogFacade<Record<string, unknown>, Record<string, unknown>, unknown>
|
||||
{
|
||||
private readonly xtreamStore = inject(XtreamStore);
|
||||
|
||||
readonly provider = 'xtream' as const;
|
||||
readonly pageSizeOptions = [10, 25, 50, 100] as const;
|
||||
readonly contentType = this.xtreamStore.selectedContentType;
|
||||
readonly limit = this.xtreamStore.limit;
|
||||
readonly pageIndex = this.xtreamStore.page;
|
||||
readonly selectedCategory = this.xtreamStore.getSelectedCategory;
|
||||
readonly paginatedContent = this.xtreamStore.getPaginatedContent;
|
||||
readonly selectedItem = this.xtreamStore.selectedItem;
|
||||
readonly totalPages = this.xtreamStore.getTotalPages;
|
||||
readonly isPaginatedContentLoading =
|
||||
this.xtreamStore.isPaginatedContentLoading;
|
||||
readonly selectedCategoryTitle = computed(() => {
|
||||
const category = this.selectedCategory();
|
||||
return String(category?.['name'] ?? category?.['title'] ?? '');
|
||||
});
|
||||
readonly categoryItemCount = computed(
|
||||
() => this.xtreamStore.selectItemsFromSelectedCategory().length
|
||||
);
|
||||
readonly contentSortMode = this.xtreamStore.contentSortMode;
|
||||
readonly playlist = computed<PortalCatalogPlaylistMeta | null>(() => {
|
||||
const playlist = this.xtreamStore.currentPlaylist();
|
||||
if (!playlist) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: String(playlist.id),
|
||||
title: playlist.name ?? playlist.title ?? 'Xtream',
|
||||
};
|
||||
});
|
||||
|
||||
initialize(categoryId?: string | null): void {
|
||||
const savedSortMode = localStorage.getItem(SORT_STORAGE_KEY);
|
||||
if (
|
||||
savedSortMode === 'date-desc' ||
|
||||
savedSortMode === 'date-asc' ||
|
||||
savedSortMode === 'name-asc' ||
|
||||
savedSortMode === 'name-desc'
|
||||
) {
|
||||
this.xtreamStore.setContentSortMode(savedSortMode);
|
||||
}
|
||||
|
||||
const playlistId = this.xtreamStore.currentPlaylist()?.id;
|
||||
if (playlistId) {
|
||||
this.xtreamStore.loadAllPositions(playlistId);
|
||||
}
|
||||
|
||||
this.clearSelectedItem();
|
||||
|
||||
if (categoryId) {
|
||||
this.xtreamStore.setSelectedCategory(Number(categoryId));
|
||||
return;
|
||||
}
|
||||
|
||||
this.xtreamStore.setSelectedCategory(null);
|
||||
}
|
||||
|
||||
clearSelectedItem(): void {
|
||||
this.xtreamStore.setSelectedItem(null);
|
||||
}
|
||||
|
||||
setPage(page: number): void {
|
||||
this.xtreamStore.setPage(page);
|
||||
}
|
||||
|
||||
setLimit(limit: number): void {
|
||||
this.xtreamStore.setLimit(limit);
|
||||
}
|
||||
|
||||
setContentSortMode(mode: PortalCatalogSortMode): void {
|
||||
this.xtreamStore.setContentSortMode(mode);
|
||||
localStorage.setItem(SORT_STORAGE_KEY, mode);
|
||||
}
|
||||
|
||||
selectItem(item: Record<string, unknown>): string[] | null {
|
||||
const xtreamId = item['xtream_id'];
|
||||
if (xtreamId === undefined || xtreamId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedCategoryId = this.xtreamStore.selectedCategoryId();
|
||||
if (selectedCategoryId !== null && selectedCategoryId !== undefined) {
|
||||
return [String(xtreamId)];
|
||||
}
|
||||
|
||||
const categoryId = item['category_id'];
|
||||
if (categoryId === undefined || categoryId === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [String(categoryId), String(xtreamId)];
|
||||
}
|
||||
|
||||
getItemProgress(
|
||||
item: Record<string, unknown>
|
||||
): PortalCatalogItemProgress {
|
||||
const isSeries = this.contentType() === 'series';
|
||||
const itemId = Number(
|
||||
item['xtream_id'] ?? item['series_id'] ?? item['stream_id']
|
||||
);
|
||||
if (Number.isNaN(itemId)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
if (isSeries) {
|
||||
return {
|
||||
hasSeriesProgress: this.xtreamStore.hasSeriesProgress(itemId),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
progress: this.xtreamStore.getProgressPercent(itemId, 'vod'),
|
||||
isWatched: this.xtreamStore.isWatched(itemId, 'vod'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function provideXtreamCatalogFacade(): Provider[] {
|
||||
return [
|
||||
XtreamCatalogFacadeService,
|
||||
{
|
||||
provide: PORTAL_CATALOG_FACADE,
|
||||
useExisting: XtreamCatalogFacadeService,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { XtreamFeatureRouteOptions } from '@iptvnator/portal/xtream/feature';
|
||||
import { provideXtreamCatalogFacade } from './xtream-catalog-facade.service';
|
||||
|
||||
// Temporary app-side loader map while route screens are still being migrated
|
||||
// out of apps/web into the Xtream feature library.
|
||||
export const xtreamFeatureRouteOptions: XtreamFeatureRouteOptions = {
|
||||
catalogProviders: provideXtreamCatalogFacade(),
|
||||
loadShellComponent: () =>
|
||||
import('./xtream-shell/xtream-shell.component').then(
|
||||
(c) => c.XtreamShellComponent
|
||||
),
|
||||
loadLiveStreamLayoutComponent: () =>
|
||||
import('./live-stream-layout/live-stream-layout.component').then(
|
||||
(c) => c.LiveStreamLayoutComponent
|
||||
),
|
||||
loadMainContainerComponent: () =>
|
||||
import('./xtream-main-container.component').then(
|
||||
(c) => c.XtreamMainContainerComponent
|
||||
),
|
||||
loadCategoryContentViewComponent: () =>
|
||||
import(
|
||||
'../portal-shared/category-content-view/category-content-view.component'
|
||||
).then((c) => c.CategoryContentViewComponent),
|
||||
loadVodDetailsRouteComponent: () =>
|
||||
import('./vod-details/vod-details-route.component').then(
|
||||
(c) => c.VodDetailsRouteComponent
|
||||
),
|
||||
loadSerialDetailsComponent: () =>
|
||||
import('./serial-details/serial-details.component').then(
|
||||
(c) => c.SerialDetailsComponent
|
||||
),
|
||||
loadFavoritesComponent: () =>
|
||||
import('./favorites/favorites.component').then(
|
||||
(c) => c.FavoritesComponent
|
||||
),
|
||||
loadRecentlyViewedComponent: () =>
|
||||
import('./recently-viewed/recently-viewed.component').then(
|
||||
(c) => c.RecentlyViewedComponent
|
||||
),
|
||||
loadSearchResultsComponent: () =>
|
||||
import('./search-results/search-results.component').then(
|
||||
(c) => c.SearchResultsComponent
|
||||
),
|
||||
loadRecentlyAddedComponent: () =>
|
||||
import('./recently-added/recently-added.component').then(
|
||||
(c) => c.RecentlyAddedComponent
|
||||
),
|
||||
};
|
||||
@@ -1,129 +0,0 @@
|
||||
import { Route } from '@angular/router';
|
||||
|
||||
export const xtreamRoutes: Route[] = [
|
||||
{
|
||||
path: 'xtreams/:id',
|
||||
loadComponent: () =>
|
||||
import('./xtream-shell/xtream-shell.component').then(
|
||||
(c) => c.XtreamShellComponent
|
||||
),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'vod',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'live',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'./live-stream-layout/live-stream-layout.component'
|
||||
).then((c) => c.LiveStreamLayoutComponent),
|
||||
children: [
|
||||
{
|
||||
path: ':categoryId',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'./live-stream-layout/live-stream-layout.component'
|
||||
).then((c) => c.LiveStreamLayoutComponent),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'vod',
|
||||
loadComponent: () =>
|
||||
import('./xtream-main-container.component').then(
|
||||
(c) => c.XtreamMainContainerComponent
|
||||
),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'../portal-shared/category-content-view/category-content-view.component'
|
||||
).then((c) => c.CategoryContentViewComponent),
|
||||
},
|
||||
{
|
||||
path: ':categoryId',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'../portal-shared/category-content-view/category-content-view.component'
|
||||
).then((c) => c.CategoryContentViewComponent),
|
||||
},
|
||||
{
|
||||
path: ':categoryId/:vodId',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'./vod-details/vod-details-route.component'
|
||||
).then((c) => c.VodDetailsRouteComponent),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'series',
|
||||
loadComponent: () =>
|
||||
import('./xtream-main-container.component').then(
|
||||
(c) => c.XtreamMainContainerComponent
|
||||
),
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'../portal-shared/category-content-view/category-content-view.component'
|
||||
).then((c) => c.CategoryContentViewComponent),
|
||||
},
|
||||
{
|
||||
path: ':categoryId',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'../portal-shared/category-content-view/category-content-view.component'
|
||||
).then((c) => c.CategoryContentViewComponent),
|
||||
},
|
||||
{
|
||||
path: ':categoryId/:serialId',
|
||||
loadComponent: () =>
|
||||
import(
|
||||
'./serial-details/serial-details.component'
|
||||
).then((c) => c.SerialDetailsComponent),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'favorites',
|
||||
loadComponent: () =>
|
||||
import('./favorites/favorites.component').then(
|
||||
(c) => c.FavoritesComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'recent',
|
||||
loadComponent: () =>
|
||||
import('./recently-viewed/recently-viewed.component').then(
|
||||
(c) => c.RecentlyViewedComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
loadComponent: () =>
|
||||
import('./search-results/search-results.component').then(
|
||||
(c) => c.SearchResultsComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'recently-added',
|
||||
loadComponent: () =>
|
||||
import('./recently-added/recently-added.component').then(
|
||||
(c) => c.RecentlyAddedComponent
|
||||
),
|
||||
},
|
||||
{
|
||||
path: 'downloads',
|
||||
loadComponent: () =>
|
||||
import('@iptvnator/portal/downloads/feature').then(
|
||||
(c) => c.DownloadsComponent
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './lib/favorites-context.service';
|
||||
export * from './lib/logger';
|
||||
export * from './lib/portal-catalog-facade';
|
||||
export * from './lib/portal-player';
|
||||
export * from './lib/portal-collection-context';
|
||||
export * from './lib/portal-collection-items';
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { InjectionToken, Signal } from '@angular/core';
|
||||
import { ResolvedPortalPlayback } from 'shared-interfaces';
|
||||
|
||||
export type PortalCatalogProvider = 'xtream' | 'stalker';
|
||||
|
||||
export type PortalCatalogSortMode =
|
||||
| 'date-desc'
|
||||
| 'date-asc'
|
||||
| 'name-asc'
|
||||
| 'name-desc';
|
||||
|
||||
export interface PortalCatalogPlaylistMeta {
|
||||
id: string;
|
||||
title?: string;
|
||||
portalUrl?: string;
|
||||
macAddress?: string;
|
||||
userAgent?: string;
|
||||
referer?: string;
|
||||
origin?: string;
|
||||
}
|
||||
|
||||
export interface PortalCatalogItemProgress {
|
||||
progress?: number;
|
||||
isWatched?: boolean;
|
||||
hasSeriesProgress?: boolean;
|
||||
}
|
||||
|
||||
export interface PortalCatalogFacade<
|
||||
TCategory = unknown,
|
||||
TItem = unknown,
|
||||
TSelectedItem = unknown,
|
||||
> {
|
||||
readonly provider: PortalCatalogProvider;
|
||||
readonly pageSizeOptions: readonly number[];
|
||||
readonly contentType: Signal<string | null | undefined>;
|
||||
readonly limit: Signal<number>;
|
||||
readonly pageIndex: Signal<number>;
|
||||
readonly selectedCategory: Signal<TCategory | null | undefined>;
|
||||
readonly paginatedContent: Signal<readonly TItem[] | undefined>;
|
||||
readonly selectedItem: Signal<TSelectedItem | null | undefined>;
|
||||
readonly totalPages: Signal<number>;
|
||||
readonly isPaginatedContentLoading: Signal<boolean>;
|
||||
readonly selectedCategoryTitle: Signal<string>;
|
||||
readonly categoryItemCount: Signal<number>;
|
||||
readonly contentSortMode: Signal<PortalCatalogSortMode | null>;
|
||||
readonly playlist: Signal<PortalCatalogPlaylistMeta | null>;
|
||||
|
||||
initialize(categoryId?: string | null): void;
|
||||
clearSelectedItem(): void;
|
||||
setPage(page: number): void;
|
||||
setLimit(limit: number): void;
|
||||
setContentSortMode(mode: PortalCatalogSortMode): void;
|
||||
selectItem(item: TItem): string[] | null;
|
||||
getItemProgress(item: TItem): PortalCatalogItemProgress;
|
||||
}
|
||||
|
||||
export interface StalkerPortalCatalogFacade<
|
||||
TCategory = unknown,
|
||||
TItem = unknown,
|
||||
TSelectedItem = unknown,
|
||||
> extends PortalCatalogFacade<TCategory, TItem, TSelectedItem> {
|
||||
readonly provider: 'stalker';
|
||||
|
||||
createLinkToPlayVod(
|
||||
cmd?: string,
|
||||
title?: string,
|
||||
thumbnail?: string
|
||||
): Promise<void>;
|
||||
addToFavorites(item: Record<string, unknown>, onDone?: () => void): void;
|
||||
removeFromFavorites(favoriteId: string, onDone?: () => void): void;
|
||||
fetchMovieFileId(itemId: string): Promise<string | null>;
|
||||
fetchLinkToPlay(
|
||||
portalUrl: string,
|
||||
macAddress: string,
|
||||
cmd: string
|
||||
): Promise<string>;
|
||||
resolveVodPlayback(
|
||||
cmd?: string,
|
||||
title?: string,
|
||||
thumbnail?: string,
|
||||
startTime?: number
|
||||
): Promise<ResolvedPortalPlayback>;
|
||||
}
|
||||
|
||||
export const PORTAL_CATALOG_FACADE = new InjectionToken<PortalCatalogFacade>(
|
||||
'PORTAL_CATALOG_FACADE'
|
||||
);
|
||||
|
||||
export function isStalkerPortalCatalogFacade(
|
||||
facade: PortalCatalogFacade
|
||||
): facade is StalkerPortalCatalogFacade {
|
||||
return facade.provider === 'stalker';
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "portal-stalker-feature",
|
||||
"$schema": "../../../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/portal/stalker/feature/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:portal", "domain:stalker", "type:feature"],
|
||||
"targets": {
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './lib/stalker-feature.routes';
|
||||
@@ -0,0 +1,113 @@
|
||||
import { Provider } from '@angular/core';
|
||||
import { Route } from '@angular/router';
|
||||
|
||||
type ComponentLoader = NonNullable<Route['loadComponent']>;
|
||||
|
||||
export interface StalkerFeatureRouteOptions {
|
||||
readonly catalogProviders?: Provider[];
|
||||
readonly loadShellComponent: ComponentLoader;
|
||||
readonly loadMainContainerComponent: ComponentLoader;
|
||||
readonly loadCategoryContentViewComponent: ComponentLoader;
|
||||
readonly loadLiveStreamLayoutComponent: ComponentLoader;
|
||||
readonly loadFavoritesComponent: ComponentLoader;
|
||||
readonly loadRecentlyViewedComponent: ComponentLoader;
|
||||
readonly loadSearchComponent: ComponentLoader;
|
||||
}
|
||||
|
||||
const loadDownloadsComponent: ComponentLoader = () =>
|
||||
import('@iptvnator/portal/downloads/feature').then(
|
||||
(c) => c.DownloadsComponent
|
||||
);
|
||||
|
||||
export function createStalkerRoutes(
|
||||
options: StalkerFeatureRouteOptions
|
||||
): Route[] {
|
||||
return [
|
||||
{
|
||||
path: 'stalker/:id',
|
||||
loadComponent: options.loadShellComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'vod',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'vod',
|
||||
providers: options.catalogProviders ?? [],
|
||||
loadComponent: options.loadMainContainerComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
data: {
|
||||
api: 'stalker',
|
||||
contentType: 'vod',
|
||||
},
|
||||
loadComponent:
|
||||
options.loadCategoryContentViewComponent,
|
||||
},
|
||||
{
|
||||
path: ':categoryId',
|
||||
data: {
|
||||
api: 'stalker',
|
||||
contentType: 'vod',
|
||||
},
|
||||
loadComponent:
|
||||
options.loadCategoryContentViewComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'vod',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'itv',
|
||||
loadComponent: options.loadLiveStreamLayoutComponent,
|
||||
},
|
||||
{
|
||||
path: 'series',
|
||||
providers: options.catalogProviders ?? [],
|
||||
loadComponent: options.loadMainContainerComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
data: {
|
||||
api: 'stalker',
|
||||
contentType: 'series',
|
||||
},
|
||||
loadComponent:
|
||||
options.loadCategoryContentViewComponent,
|
||||
},
|
||||
{
|
||||
path: ':categoryId',
|
||||
data: {
|
||||
api: 'stalker',
|
||||
contentType: 'series',
|
||||
},
|
||||
loadComponent:
|
||||
options.loadCategoryContentViewComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'favorites',
|
||||
loadComponent: options.loadFavoritesComponent,
|
||||
},
|
||||
{
|
||||
path: 'recent',
|
||||
loadComponent: options.loadRecentlyViewedComponent,
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
loadComponent: options.loadSearchComponent,
|
||||
},
|
||||
{
|
||||
path: 'downloads',
|
||||
loadComponent: loadDownloadsComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"isolatedModules": true,
|
||||
"target": "es2022",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"emitDecoratorMetadata": false,
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "portal-xtream-feature",
|
||||
"$schema": "../../../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/portal/xtream/feature/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:portal", "domain:xtream", "type:feature"],
|
||||
"targets": {
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './lib/xtream-feature.routes';
|
||||
@@ -0,0 +1,115 @@
|
||||
import { Provider } from '@angular/core';
|
||||
import { Route } from '@angular/router';
|
||||
|
||||
type ComponentLoader = NonNullable<Route['loadComponent']>;
|
||||
|
||||
export interface XtreamFeatureRouteOptions {
|
||||
readonly catalogProviders?: Provider[];
|
||||
readonly loadShellComponent: ComponentLoader;
|
||||
readonly loadLiveStreamLayoutComponent: ComponentLoader;
|
||||
readonly loadMainContainerComponent: ComponentLoader;
|
||||
readonly loadCategoryContentViewComponent: ComponentLoader;
|
||||
readonly loadVodDetailsRouteComponent: ComponentLoader;
|
||||
readonly loadSerialDetailsComponent: ComponentLoader;
|
||||
readonly loadFavoritesComponent: ComponentLoader;
|
||||
readonly loadRecentlyViewedComponent: ComponentLoader;
|
||||
readonly loadSearchResultsComponent: ComponentLoader;
|
||||
readonly loadRecentlyAddedComponent: ComponentLoader;
|
||||
}
|
||||
|
||||
const loadDownloadsComponent: ComponentLoader = () =>
|
||||
import('@iptvnator/portal/downloads/feature').then(
|
||||
(c) => c.DownloadsComponent
|
||||
);
|
||||
|
||||
export function createXtreamRoutes(
|
||||
options: XtreamFeatureRouteOptions
|
||||
): Route[] {
|
||||
return [
|
||||
{
|
||||
path: 'xtreams/:id',
|
||||
loadComponent: options.loadShellComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'vod',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'live',
|
||||
loadComponent: options.loadLiveStreamLayoutComponent,
|
||||
children: [
|
||||
{
|
||||
path: ':categoryId',
|
||||
loadComponent:
|
||||
options.loadLiveStreamLayoutComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'vod',
|
||||
providers: options.catalogProviders ?? [],
|
||||
loadComponent: options.loadMainContainerComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadComponent:
|
||||
options.loadCategoryContentViewComponent,
|
||||
},
|
||||
{
|
||||
path: ':categoryId',
|
||||
loadComponent:
|
||||
options.loadCategoryContentViewComponent,
|
||||
},
|
||||
{
|
||||
path: ':categoryId/:vodId',
|
||||
loadComponent:
|
||||
options.loadVodDetailsRouteComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'series',
|
||||
providers: options.catalogProviders ?? [],
|
||||
loadComponent: options.loadMainContainerComponent,
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
loadComponent:
|
||||
options.loadCategoryContentViewComponent,
|
||||
},
|
||||
{
|
||||
path: ':categoryId',
|
||||
loadComponent:
|
||||
options.loadCategoryContentViewComponent,
|
||||
},
|
||||
{
|
||||
path: ':categoryId/:serialId',
|
||||
loadComponent: options.loadSerialDetailsComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'favorites',
|
||||
loadComponent: options.loadFavoritesComponent,
|
||||
},
|
||||
{
|
||||
path: 'recent',
|
||||
loadComponent: options.loadRecentlyViewedComponent,
|
||||
},
|
||||
{
|
||||
path: 'search',
|
||||
loadComponent: options.loadSearchResultsComponent,
|
||||
},
|
||||
{
|
||||
path: 'recently-added',
|
||||
loadComponent: options.loadRecentlyAddedComponent,
|
||||
},
|
||||
{
|
||||
path: 'downloads',
|
||||
loadComponent: loadDownloadsComponent,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"extends": "../../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"isolatedModules": true,
|
||||
"target": "es2022",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"emitDecoratorMetadata": false,
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
|
||||
}
|
||||
@@ -24,9 +24,15 @@
|
||||
"@iptvnator/portal/shared/util": [
|
||||
"libs/portal/shared/util/src/index.ts"
|
||||
],
|
||||
"@iptvnator/portal/stalker/feature": [
|
||||
"libs/portal/stalker/feature/src/index.ts"
|
||||
],
|
||||
"@iptvnator/portal/stalker/data-access": [
|
||||
"libs/portal/stalker/data-access/src/index.ts"
|
||||
],
|
||||
"@iptvnator/portal/xtream/feature": [
|
||||
"libs/portal/xtream/feature/src/index.ts"
|
||||
],
|
||||
"@iptvnator/portal/xtream/data-access": [
|
||||
"libs/portal/xtream/data-access/src/index.ts"
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user