refactor(nx): update repository structure

Introduce new libraries to improve the boundaries and dependencies in mono repo
This commit is contained in:
4gray committed 2026-03-08 20:01:42 +01:00
1 parent 1780bb0e53
commit 256f0e0136
198 files changed
+1384 -815

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@
"projectType": "application",
"prefix": "app",
"sourceRoot": "apps/web/src",
"tags": [],
"tags": ["scope:app", "type:app"],
"targets": {
"build": {
"executor": "@angular/build:application",
+7 -1
View File
@@ -20,13 +20,15 @@ import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { PlaylistEffects, playlistReducer } from 'm3u-state';
import { NgxIndexedDBModule, NgxIndexedDBService } from 'ngx-indexed-db';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { PORTAL_PLAYER } from '@iptvnator/portal/shared/util';
import { provideXtreamDataSource } from '@iptvnator/portal/xtream/data-access';
import { DataService } from 'services';
import { dbConfig } from 'shared-interfaces';
import { AppConfig } from '../environments/environment';
import { routes } from './app.routes';
import { ElectronService } from './services/electron.service';
import { PlayerService } from './services/player.service';
import { PwaService } from './services/pwa.service';
import { provideXtreamDataSource } from './xtream-electron/data-sources';
// AoT requires an exported function for factories
export function HttpLoaderFactory(http: HttpClient): TranslateHttpLoader {
@@ -85,6 +87,10 @@ export const appConfig: ApplicationConfig = {
useFactory: DataFactory,
deps: [NgxIndexedDBService, HttpClient],
},
{
provide: PORTAL_PLAYER,
useExisting: PlayerService,
},
...provideXtreamDataSource(),
],
};
+1 -1
View File
@@ -76,7 +76,7 @@ export const routes: Routes = [
path: 'downloads',
data: { layout: 'workspace' },
loadComponent: () =>
import('./xtream-electron/downloads/downloads.component').then(
import('@iptvnator/portal/downloads/feature').then(
(c) => c.DownloadsComponent
),
},
@@ -28,9 +28,9 @@ 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 '../../shared/components/playlist-error-view/playlist-error-view.component';
import { createLogger } from '../../shared/utils/logger';
import { StalkerVodSource } from '../../stalker/models';
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,
@@ -40,12 +40,12 @@ import {
normalizeStalkerEntityId,
normalizeStalkerEntityIdAsNumber,
toggleStalkerVodFavorite,
} from '../../stalker/stalker-vod.utils';
import { StalkerStore } from '../../stalker/stalker.store';
import { XTREAM_DATA_SOURCE } from '../data-sources';
import { XtreamCategorySortMode } from '../stores/features/with-selection.feature';
import { XtreamStore } from '../stores/xtream.store';
import { VodDetailsComponent } from '../vod-details/vod-details.component';
} 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 { VodDetailsComponent } from '../../xtream-electron/vod-details/vod-details.component';
const SORT_STORAGE_KEY = 'xtream-category-sort-mode';
@@ -10,10 +10,10 @@ import { PlaylistInfoComponent } from 'components';
import { selectPlaylistById } from 'm3u-state';
import { Playlist } from 'shared-interfaces';
import { SettingsComponent } from '../../settings/settings.component';
import { AccountInfoComponent } from '../account-info/account-info.component';
import { XtreamStore } from '../stores/xtream.store';
import { buildPortalRailLinks } from '../../shared/navigation/portal-rail-links';
import { PortalRailLinksComponent } from '../../shared/navigation/portal-rail-links.component';
import { AccountInfoComponent } from '../../xtream-electron/account-info/account-info.component';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import { buildPortalRailLinks } from '@iptvnator/portal/shared/util';
import { PortalRailLinksComponent } from '@iptvnator/portal/shared/ui';
@Component({
selector: 'app-navigation',
+5 -424
View File
@@ -1,424 +1,5 @@
import { computed, inject, Injectable, OnDestroy, signal } from '@angular/core';
import { SettingsStore } from './settings-store.service';
export type DownloadStatus =
| 'queued'
| 'downloading'
| 'completed'
| 'failed'
| 'canceled';
export interface DownloadItem {
id: number;
playlistId: string;
xtreamId: number;
contentType: 'vod' | 'episode';
seriesXtreamId?: number;
seasonNumber?: number;
episodeNumber?: number;
title: string;
url: string;
fileName?: string;
filePath?: string;
posterUrl?: string;
status: DownloadStatus;
bytesDownloaded?: number;
totalBytes?: number;
errorMessage?: string;
createdAt?: string;
updatedAt?: string;
}
@Injectable({ providedIn: 'root' })
export class DownloadsService implements OnDestroy {
private readonly settingsStore = inject(SettingsStore);
private unsubscribe?: () => void;
/** Signal for the list of downloads */
readonly downloads = signal<DownloadItem[]>([]);
/** Whether the download feature is available (Electron only) */
readonly isAvailable = computed(() => !!window.electron?.downloadsGetList);
/** Whether there are any downloads */
readonly hasDownloads = computed(() => this.downloads().length > 0);
/** Active downloads count */
readonly activeCount = computed(
() =>
this.downloads().filter(
(d) => d.status === 'queued' || d.status === 'downloading'
).length
);
/** Completed downloads count */
readonly completedCount = computed(
() => this.downloads().filter((d) => d.status === 'completed').length
);
/** Failed downloads count */
readonly failedCount = computed(
() =>
this.downloads().filter(
(d) => d.status === 'failed' || d.status === 'canceled'
).length
);
/** Current download folder */
readonly downloadFolder = signal<string>('');
constructor() {
this.init();
}
private async init() {
if (!this.isAvailable()) {
return;
}
// Load initial download list
await this.loadDownloads();
// Load download folder
await this.loadDownloadFolder();
// Subscribe to download updates
this.unsubscribe = window.electron.onDownloadsUpdate(() => {
this.loadDownloads();
});
}
ngOnDestroy() {
this.unsubscribe?.();
}
/**
* Load downloads from the backend
*/
async loadDownloads(playlistId?: string): Promise<void> {
if (!this.isAvailable()) return;
try {
const list = await window.electron.downloadsGetList(playlistId);
this.downloads.set(list);
} catch (error) {
console.error('[DownloadsService] Error loading downloads:', error);
}
}
/**
* Load or resolve download folder
*/
async loadDownloadFolder(): Promise<string> {
if (!this.isAvailable()) return '';
// First check settings
const storedFolder = this.settingsStore.getDownloadFolder?.();
if (storedFolder) {
this.downloadFolder.set(storedFolder);
return storedFolder;
}
// Fall back to default
try {
const defaultFolder =
await window.electron.downloadsGetDefaultFolder();
this.downloadFolder.set(defaultFolder);
return defaultFolder;
} catch (error) {
console.error(
'[DownloadsService] Error getting default folder:',
error
);
return '';
}
}
/**
* Start a new download
*/
async startDownload(data: {
playlistId: string;
xtreamId: number;
contentType: 'vod' | 'episode';
title: string;
url: string;
posterUrl?: string;
headers?: { userAgent?: string; referer?: string; origin?: string };
seriesXtreamId?: number;
seasonNumber?: number;
episodeNumber?: number;
// Playlist info for auto-creation if needed (Stalker playlists)
playlistName?: string;
playlistType?: 'xtream' | 'stalker' | 'm3u-file' | 'm3u-text' | 'm3u-url';
serverUrl?: string;
portalUrl?: string;
macAddress?: string;
}): Promise<{ success: boolean; id?: number; error?: string }> {
if (!this.isAvailable()) {
return { success: false, error: 'Downloads not available' };
}
const folder = await this.loadDownloadFolder();
if (!folder) {
return { success: false, error: 'No download folder configured' };
}
try {
const result = await window.electron.downloadsStart({
...data,
downloadFolder: folder,
});
return result;
} catch (error) {
console.error('[DownloadsService] Error starting download:', error);
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* Cancel a download
*/
async cancelDownload(
downloadId: number
): Promise<{ success: boolean; error?: string }> {
if (!this.isAvailable()) {
return { success: false, error: 'Downloads not available' };
}
try {
return await window.electron.downloadsCancel(downloadId);
} catch (error) {
console.error(
'[DownloadsService] Error canceling download:',
error
);
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* Retry a failed download
*/
async retryDownload(
downloadId: number
): Promise<{ success: boolean; error?: string }> {
if (!this.isAvailable()) {
return { success: false, error: 'Downloads not available' };
}
const folder = await this.loadDownloadFolder();
if (!folder) {
return { success: false, error: 'No download folder configured' };
}
try {
return await window.electron.downloadsRetry(downloadId, folder);
} catch (error) {
console.error(
'[DownloadsService] Error retrying download:',
error
);
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* Remove a download from the list
*/
async removeDownload(
downloadId: number
): Promise<{ success: boolean; error?: string }> {
if (!this.isAvailable()) {
return { success: false, error: 'Downloads not available' };
}
try {
return await window.electron.downloadsRemove(downloadId);
} catch (error) {
console.error(
'[DownloadsService] Error removing download:',
error
);
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* Play a downloaded file
*/
async playDownload(
filePath: string
): Promise<{ success: boolean; error?: string }> {
if (!this.isAvailable()) {
return { success: false, error: 'Downloads not available' };
}
try {
return await window.electron.downloadsPlayFile(filePath);
} catch (error) {
console.error('[DownloadsService] Error playing file:', error);
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* Reveal file in system file manager
*/
async revealFile(
filePath: string
): Promise<{ success: boolean; error?: string }> {
if (!this.isAvailable()) {
return { success: false, error: 'Downloads not available' };
}
try {
return await window.electron.downloadsRevealFile(filePath);
} catch (error) {
console.error('[DownloadsService] Error revealing file:', error);
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}
/**
* Select a new download folder
*/
async selectFolder(): Promise<string | null> {
if (!this.isAvailable()) {
return null;
}
try {
const folder = await window.electron.downloadsSelectFolder();
if (folder) {
this.downloadFolder.set(folder);
// Save to settings
await this.settingsStore.updateSettings({ downloadFolder: folder });
}
return folder;
} catch (error) {
console.error(
'[DownloadsService] Error selecting folder:',
error
);
return null;
}
}
/**
* Clear completed/failed downloads
*/
async clearCompleted(
playlistId?: string
): Promise<{ success: boolean }> {
if (!this.isAvailable()) {
return { success: false };
}
try {
return await window.electron.downloadsClearCompleted(playlistId);
} catch (error) {
console.error(
'[DownloadsService] Error clearing completed:',
error
);
return { success: false };
}
}
/**
* Get download progress as percentage
*/
getProgressPercent(item: DownloadItem): number {
if (!item.totalBytes || item.totalBytes === 0) {
return 0;
}
return Math.round(
((item.bytesDownloaded || 0) / item.totalBytes) * 100
);
}
/**
* Get download item by xtreamId and playlistId
*/
getDownloadByContent(
xtreamId: number,
playlistId: string,
contentType: 'vod' | 'episode'
): DownloadItem | undefined {
return this.downloads().find(
(d) =>
d.xtreamId === xtreamId &&
d.playlistId === playlistId &&
d.contentType === contentType
);
}
/**
* Check if content is downloaded (completed status)
*/
isDownloaded(
xtreamId: number,
playlistId: string,
contentType: 'vod' | 'episode'
): boolean {
const download = this.getDownloadByContent(xtreamId, playlistId, contentType);
return download?.status === 'completed' && !!download.filePath;
}
/**
* Check if content is currently downloading or queued
*/
isDownloading(
xtreamId: number,
playlistId: string,
contentType: 'vod' | 'episode'
): boolean {
const download = this.getDownloadByContent(xtreamId, playlistId, contentType);
return download?.status === 'downloading' || download?.status === 'queued';
}
/**
* Get the file path for a downloaded content item
*/
getDownloadedFilePath(
xtreamId: number,
playlistId: string,
contentType: 'vod' | 'episode'
): string | undefined {
const download = this.getDownloadByContent(xtreamId, playlistId, contentType);
if (download?.status === 'completed') {
return download.filePath;
}
return undefined;
}
/**
* Format bytes to human readable string
*/
formatBytes(bytes: number): string {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
}
export {
DownloadsService,
type DownloadItem,
type DownloadStatus,
} from 'services';
@@ -18,7 +18,7 @@ import { AppConfig } from '../../environments/environment';
import {
createPortalDebugRequestContext,
logPortalDebugEvent,
} from '../shared/utils/logger';
} from '@iptvnator/portal/shared/util';
@Injectable({
providedIn: 'root',
+1 -1
View File
@@ -25,7 +25,7 @@ import {
createPortalDebugSuccessEvent,
logPortalDebugEvent,
logPortalDebugRequest,
} from '../shared/utils/logger';
} from '@iptvnator/portal/shared/util';
@Injectable({
providedIn: 'root',
@@ -1,109 +1 @@
import { inject } from '@angular/core';
import {
patchState,
signalStore,
withHooks,
withMethods,
withState,
} from '@ngrx/signals';
import { StorageMap } from '@ngx-pwa/local-storage';
import { firstValueFrom } from 'rxjs';
import {
Language,
Settings,
STORE_KEY,
StreamFormat,
Theme,
VideoPlayer,
} from 'shared-interfaces';
const DEFAULT_SETTINGS: Settings = {
player: VideoPlayer.VideoJs,
streamFormat: StreamFormat.M3u8StreamFormat,
language: Language.ENGLISH,
showCaptions: false,
showExternalPlaybackBar: true,
theme: Theme.LightTheme,
mpvPlayerPath: '',
mpvReuseInstance: false,
vlcPlayerPath: '',
remoteControl: false,
remoteControlPort: 8765,
epgUrl: [],
downloadFolder: '',
};
export const SettingsStore = signalStore(
{ providedIn: 'root' },
withState<Settings>(DEFAULT_SETTINGS),
withMethods((store, storage = inject(StorageMap)) => ({
async loadSettings() {
try {
const stored = await firstValueFrom(
storage.get(STORE_KEY.Settings)
);
if (stored) {
patchState(store, {
...DEFAULT_SETTINGS,
...(stored as Settings),
});
}
} catch (error) {
console.error('Failed to load settings:', error);
// Keep default settings if loading fails
}
},
async updateSettings(settings: Partial<Settings>) {
patchState(store, settings);
// Save the complete settings object, not just the partial update
const completeSettings = this.getSettings();
try {
await firstValueFrom(
storage.set(STORE_KEY.Settings, completeSettings)
);
} catch (error) {
console.error('Failed to save settings:', error);
throw error;
}
},
getSettings() {
return {
player: store.player(),
streamFormat: store.streamFormat(),
language: store.language(),
showCaptions: store.showCaptions(),
showExternalPlaybackBar: store.showExternalPlaybackBar(),
theme: store.theme(),
mpvPlayerPath: store.mpvPlayerPath(),
mpvReuseInstance: store.mpvReuseInstance(),
vlcPlayerPath: store.vlcPlayerPath(),
remoteControl: store.remoteControl(),
remoteControlPort: store.remoteControlPort(),
epgUrl: store.epgUrl(),
downloadFolder: store.downloadFolder(),
};
},
getDownloadFolder() {
return store.downloadFolder();
},
getPlayer() {
return store.player();
},
isEmbeddedPlayer() {
return (
store.player() === VideoPlayer.VideoJs ||
store.player() === VideoPlayer.Html5Player
);
},
})),
withHooks({
onInit(store) {
store.loadSettings();
},
})
);
export { SettingsStore } from 'services';
@@ -4,7 +4,7 @@ import { MatIcon } from '@angular/material/icon';
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatTooltip } from '@angular/material/tooltip';
import { TranslatePipe } from '@ngx-translate/core';
import { PlaylistErrorViewComponent } from '../playlist-error-view/playlist-error-view.component';
import { PlaylistErrorViewComponent } from '@iptvnator/portal/shared/ui';
import { ProgressCapsuleComponent } from '../../../xtream-electron/shared/progress-capsule/progress-capsule.component';
import { WatchedBadgeComponent } from '../../../xtream-electron/shared/watched-badge/watched-badge.component';
+11 -6
View File
@@ -1,7 +1,12 @@
export * from './category-view/category-view.component';
export * from './content-card/content-card.component';
export {
CategoryViewComponent,
ContentCardComponent,
FavoritesLayoutComponent,
PlaylistErrorViewComponent,
PortalCollectionLiveShellComponent,
PortalCollectionShellComponent,
PortalEmptyStateComponent,
SearchFormComponent,
SearchLayoutComponent,
} from '@iptvnator/portal/shared/ui';
export * from './header/header.component';
export * from './playlist-error-view/playlist-error-view.component';
export * from './portal-collection-shell/portal-collection-shell.component';
export * from './portal-empty-state/portal-empty-state.component';
export * from './stalker-inline-detail/stalker-inline-detail.component';
@@ -1,3 +1 @@
// Canonical source: libs/ui/styles/_content-grid.scss
// This stub forwards the mixins for components that @use this file directly.
@forward '../../../../../../libs/ui/styles/content-grid';
@forward '../../../../../../libs/portal/shared/ui/src/lib/styles/content-grid';
@@ -1,3 +1 @@
// Canonical source: libs/ui/styles/_portal-layout.scss
// This stub forwards the mixin for components that @use this file directly.
@forward '../../../../../../libs/ui/styles/portal-layout';
@forward '../../../../../../libs/portal/shared/ui/src/lib/styles/portal-layout';
@@ -1,3 +1 @@
// Canonical source: libs/ui/styles/_portal-sidebar.scss
// This stub forwards the rules for components that @use this file directly.
@forward '../../../../../../libs/ui/styles/portal-sidebar';
@forward '../../../../../../libs/portal/shared/ui/src/lib/styles/portal-sidebar';
@@ -4,8 +4,8 @@ import { MatIconModule } from '@angular/material/icon';
import { TranslateModule } from '@ngx-translate/core';
import { BehaviorSubject, map, switchMap } from 'rxjs';
import { PlaylistsService } from 'services';
import { StalkerStore } from '../stalker.store';
import { matchesFavoriteById } from '../stalker-vod.utils';
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
import { matchesFavoriteById } from '@iptvnator/portal/stalker/data-access';
@Component({
selector: 'app-favorites-button',
@@ -11,25 +11,25 @@ import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { PlaylistsService } from 'services';
import { EpgItem, VodDetailsItem } from 'shared-interfaces';
import { PlayerService } from '../../services/player.service';
import { PortalCollectionLiveShellComponent } from '../../shared/components/portal-collection-live-shell/portal-collection-live-shell.component';
import { PortalCollectionLiveShellComponent } from '@iptvnator/portal/shared/ui';
import {
PortalCollectionMode,
PortalCollectionShellComponent,
PortalCollectionShellLayout,
} from '../../shared/components/portal-collection-shell/portal-collection-shell.component';
import { StalkerInlineDetailComponent } from '../../shared/components/stalker-inline-detail/stalker-inline-detail.component';
} from '@iptvnator/portal/shared/ui';
import { StalkerInlineDetailComponent } from '../stalker-inline-detail/stalker-inline-detail.component';
import {
isWorkspaceLayoutRoute,
queryParamSignal,
} from '../../shared/navigation/portal-route.utils';
import { createPortalCollectionContext } from '../../shared/utils/portal-collection-context';
} from '@iptvnator/portal/shared/util';
import { createPortalCollectionContext } from '@iptvnator/portal/shared/util';
import {
buildStandardCollectionCategories,
filterCollectionBucket,
} from '../../shared/utils/portal-collection-items';
import { createLogger } from '../../shared/utils/logger';
import { FavoritesContextService } from '../../workspace/favorites-context.service';
import { StalkerSelectedVodItem, StalkerVodSource } from '../models';
} from '@iptvnator/portal/shared/util';
import { createLogger } from '@iptvnator/portal/shared/util';
import { FavoritesContextService } from '@iptvnator/portal/shared/util';
import { StalkerSelectedVodItem, StalkerVodSource } from '@iptvnator/portal/stalker/data-access';
import {
clearStalkerDetailViewState,
createStalkerInlineDetailState,
@@ -40,9 +40,9 @@ import {
isSelectedStalkerVodFavorite,
normalizeStalkerVodDetailsItem,
toggleStalkerVodFavorite,
} from '../stalker-vod.utils';
} from '@iptvnator/portal/stalker/data-access';
import { StalkerCollectionChannelsListComponent } from '../stalker-collection-channels-list/stalker-collection-channels-list.component';
import { StalkerStore } from '../stalker.store';
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
const STALKER_RECENT_LAYOUT: Omit<
PortalCollectionShellLayout,
@@ -13,9 +13,9 @@ import { MatIconModule } from '@angular/material/icon';
import { TranslatePipe } from '@ngx-translate/core';
import { ChannelListItemComponent } from 'components';
import { EpgItem, EpgProgram } from 'shared-interfaces';
import { StalkerVodSource } from '../models';
import { normalizeStalkerEntityId } from '../stalker-vod.utils';
import { StalkerStore } from '../stalker.store';
import { StalkerVodSource } from '@iptvnator/portal/stalker/data-access';
import { normalizeStalkerEntityId } from '@iptvnator/portal/stalker/data-access';
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
@Component({
selector: 'app-stalker-collection-channels-list',
@@ -11,22 +11,22 @@ import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { PlaylistsService } from 'services';
import { EpgItem, VodDetailsItem } from 'shared-interfaces';
import { PlayerService } from '../../services/player.service';
import { PortalCollectionLiveShellComponent } from '../../shared/components/portal-collection-live-shell/portal-collection-live-shell.component';
import { PortalCollectionLiveShellComponent } from '@iptvnator/portal/shared/ui';
import {
PortalCollectionMode,
PortalCollectionShellComponent,
PortalCollectionShellLayout,
} from '../../shared/components/portal-collection-shell/portal-collection-shell.component';
import { StalkerInlineDetailComponent } from '../../shared/components/stalker-inline-detail/stalker-inline-detail.component';
import { queryParamSignal } from '../../shared/navigation/portal-route.utils';
import { createPortalCollectionContext } from '../../shared/utils/portal-collection-context';
} from '@iptvnator/portal/shared/ui';
import { StalkerInlineDetailComponent } from '../stalker-inline-detail/stalker-inline-detail.component';
import { queryParamSignal } from '@iptvnator/portal/shared/util';
import { createPortalCollectionContext } from '@iptvnator/portal/shared/util';
import {
buildStandardCollectionCategories,
filterCollectionBucket,
} from '../../shared/utils/portal-collection-items';
import { createLogger } from '../../shared/utils/logger';
import { FavoritesContextService } from '../../workspace/favorites-context.service';
import { StalkerFavoriteItem } from '../models';
} from '@iptvnator/portal/shared/util';
import { createLogger } from '@iptvnator/portal/shared/util';
import { FavoritesContextService } from '@iptvnator/portal/shared/util';
import { StalkerFavoriteItem } from '@iptvnator/portal/stalker/data-access';
import {
clearStalkerDetailViewState,
createStalkerInlineDetailState,
@@ -38,9 +38,9 @@ import {
normalizeStalkerEntityId,
normalizeStalkerFavoriteItem,
toggleStalkerVodFavorite,
} from '../stalker-vod.utils';
} from '@iptvnator/portal/stalker/data-access';
import { StalkerCollectionChannelsListComponent } from '../stalker-collection-channels-list/stalker-collection-channels-list.component';
import { StalkerStore } from '../stalker.store';
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
const STALKER_FAVORITES_LAYOUT: PortalCollectionShellLayout = {};
@@ -5,11 +5,11 @@ import {
input,
output,
} from '@angular/core';
import { ExternalPlaybackService } from '../../../services/external-playback.service';
import { ExternalPlaybackService } from '../../services/external-playback.service';
import { VodDetailsItem } from 'shared-interfaces';
import { VodDetailsComponent } from '../../../xtream-electron/vod-details/vod-details.component';
import { StalkerSelectedVodItem } from '../../../stalker/models/stalker-favorite-item.interface';
import { StalkerSeriesViewComponent } from '../../../stalker/stalker-series-view/stalker-series-view.component';
import { VodDetailsComponent } from '../../xtream-electron/vod-details/vod-details.component';
import { StalkerSelectedVodItem } from '@iptvnator/portal/stalker/data-access';
import { StalkerSeriesViewComponent } from '../stalker-series-view/stalker-series-view.component';
@Component({
selector: 'app-stalker-inline-detail',
@@ -31,22 +31,22 @@ import { EpgItem, EpgProgram } from 'shared-interfaces';
import { EpgViewComponent, WebPlayerViewComponent } from 'shared-portals';
import { PlayerService } from '../../services/player.service';
import { SettingsStore } from '../../services/settings-store.service';
import { CategoryViewComponent } from '../../shared/components/category-view/category-view.component';
import { PlaylistErrorViewComponent } from '../../shared/components/playlist-error-view/playlist-error-view.component';
import { PortalEmptyStateComponent } from '../../shared/components/portal-empty-state/portal-empty-state.component';
import { isWorkspaceLayoutRoute } from '../../shared/navigation/portal-route.utils';
import { CategoryViewComponent } from '@iptvnator/portal/shared/ui';
import { PlaylistErrorViewComponent } from '@iptvnator/portal/shared/ui';
import { PortalEmptyStateComponent } from '@iptvnator/portal/shared/ui';
import { isWorkspaceLayoutRoute } from '@iptvnator/portal/shared/util';
import {
getAdjacentChannelItem,
getChannelItemByNumber,
} from '../../shared/services/remote-channel-navigation.util';
import { createLogger } from '../../shared/utils/logger';
import { createLogger } from '@iptvnator/portal/shared/util';
import {
StalkerCategoryItem,
StalkerFavoriteItem,
StalkerItvChannel,
} from '../models';
import { normalizeStalkerEntityId } from '../stalker-vod.utils';
import { StalkerStore } from '../stalker.store';
} from '@iptvnator/portal/stalker/data-access';
import { normalizeStalkerEntityId } from '@iptvnator/portal/stalker/data-access';
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
@Component({
selector: 'app-stalker-live-stream-layout',
@@ -5,10 +5,10 @@ import { MatPaginatorModule } from '@angular/material/paginator';
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { PlaylistSwitcherComponent, ResizableDirective } from 'components';
import { CategoryViewComponent } from '../shared/components/category-view/category-view.component';
import { PlaylistErrorViewComponent } from '../shared/components/playlist-error-view/playlist-error-view.component';
import { isWorkspaceLayoutRoute } from '../shared/navigation/portal-route.utils';
import { StalkerStore } from './stalker.store';
import { CategoryViewComponent } from '@iptvnator/portal/shared/ui';
import { PlaylistErrorViewComponent } from '@iptvnator/portal/shared/ui';
import { isWorkspaceLayoutRoute } from '@iptvnator/portal/shared/util';
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
@Component({
selector: 'app-stalker-main-container',
@@ -12,20 +12,20 @@ import {
StalkerPortalActions,
VodDetailsItem,
} from 'shared-interfaces';
import { ContentCardComponent } from '../../shared/components/content-card/content-card.component';
import { SearchLayoutComponent } from '../../shared/components/search-layout/search-layout.component';
import { StalkerInlineDetailComponent } from '../../shared/components/stalker-inline-detail/stalker-inline-detail.component';
import { StalkerContentTypes } from '../stalker-content-types';
import { StalkerStore } from '../stalker.store';
import { ContentCardComponent } from '@iptvnator/portal/shared/ui';
import { SearchLayoutComponent } from '@iptvnator/portal/shared/ui';
import { StalkerInlineDetailComponent } from '../stalker-inline-detail/stalker-inline-detail.component';
import { StalkerContentTypes } from '@iptvnator/portal/stalker/data-access';
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
import {
isWorkspaceLayoutRoute,
queryParamSignal,
} from '../../shared/navigation/portal-route.utils';
import { createLogger } from '../../shared/utils/logger';
} from '@iptvnator/portal/shared/util';
import { createLogger } from '@iptvnator/portal/shared/util';
import {
StalkerSelectedVodItem,
StalkerVodSource,
} from '../models';
} from '@iptvnator/portal/stalker/data-access';
import {
buildStalkerSelectedVodItem,
clearStalkerDetailViewState,
@@ -36,7 +36,7 @@ import {
isSelectedStalkerVodFavorite,
isStalkerSeriesFlag,
toggleStalkerVodFavorite,
} from '../stalker-vod.utils';
} from '@iptvnator/portal/stalker/data-access';
interface StalkerFilter {
key: string;
@@ -11,14 +11,14 @@ import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { FavoritesButtonComponent } from '../favorites-button/favorites-button.component';
import { ContentHeroComponent } from 'components';
import { StalkerStore } from '../stalker.store';
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
import { SeasonContainerComponent } from '../../xtream-electron/season-container/season-container.component';
import {
ResolvedPortalPlayback,
XtreamSerieEpisode,
} from 'shared-interfaces';
import { XtreamStore } from '../../xtream-electron/stores/xtream.store';
import { createLogger } from '../../shared/utils/logger';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import { createLogger } from '@iptvnator/portal/shared/util';
import {
getVodSeriesSeasonKey,
isVodSeriesItem,
@@ -29,9 +29,9 @@ import {
StalkerMappedEpisode,
StalkerSeriesSeasonVm,
VodSeriesSeasonVm,
} from '../stalker-series.adapters';
import { StalkerSelectedVodItem, StalkerVodSource } from '../models';
import { normalizeStalkerVodDetailsItem } from '../stalker-vod.utils';
} from '@iptvnator/portal/stalker/data-access';
import { StalkerSelectedVodItem, StalkerVodSource } from '@iptvnator/portal/stalker/data-access';
import { normalizeStalkerVodDetailsItem } from '@iptvnator/portal/stalker/data-access';
import { PlayerService } from '../../services/player.service';
import { ExternalPlaybackService } from '../../services/external-playback.service';
import { PortalInlinePlayerComponent } from '../../shared/components/portal-inline-player/portal-inline-player.component';
@@ -9,15 +9,15 @@ import {
import { Store } from '@ngrx/store';
import { PlaylistActions, selectPlaylistById } from 'm3u-state';
import { map, switchMap } from 'rxjs';
import { PortalRailSection } from '../../shared/navigation/portal-rail-links';
import { PortalRailSection } from '@iptvnator/portal/shared/util';
import {
isWorkspaceLayoutRoute,
resolveCurrentPortalSection,
} from '../../shared/navigation/portal-route.utils';
import { createLogger } from '../../shared/utils/logger';
import { NavigationComponent } from '../../xtream-electron/navigation/navigation.component';
import { XtreamStore } from '../../xtream-electron/stores/xtream.store';
import { StalkerStore } from '../stalker.store';
} from '@iptvnator/portal/shared/util';
import { createLogger } from '@iptvnator/portal/shared/util';
import { NavigationComponent } from '../../portal-shared/navigation/navigation.component';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
@Component({
selector: 'app-stalker-shell',
+5 -5
View File
@@ -28,7 +28,7 @@ export const stalkerRoutes: Route[] = [
},
loadComponent: () =>
import(
'../xtream-electron/category-content-view/category-content-view.component'
'../portal-shared/category-content-view/category-content-view.component'
).then((c) => c.CategoryContentViewComponent),
},
{
@@ -39,7 +39,7 @@ export const stalkerRoutes: Route[] = [
},
loadComponent: () =>
import(
'../xtream-electron/category-content-view/category-content-view.component'
'../portal-shared/category-content-view/category-content-view.component'
).then((c) => c.CategoryContentViewComponent),
},
],
@@ -68,7 +68,7 @@ export const stalkerRoutes: Route[] = [
path: '',
loadComponent: () =>
import(
'../xtream-electron/category-content-view/category-content-view.component'
'../portal-shared/category-content-view/category-content-view.component'
).then((c) => c.CategoryContentViewComponent),
data: {
api: 'stalker',
@@ -79,7 +79,7 @@ export const stalkerRoutes: Route[] = [
path: ':categoryId',
loadComponent: () =>
import(
'../xtream-electron/category-content-view/category-content-view.component'
'../portal-shared/category-content-view/category-content-view.component'
).then((c) => c.CategoryContentViewComponent),
data: {
api: 'stalker',
@@ -113,7 +113,7 @@ export const stalkerRoutes: Route[] = [
path: 'downloads',
loadComponent: () =>
import(
'../xtream-electron/downloads/downloads.component'
'@iptvnator/portal/downloads/feature'
).then((c) => c.DownloadsComponent),
},
],
@@ -1,35 +1 @@
import { Injectable, signal } from '@angular/core';
import { XtreamCategory } from 'shared-interfaces';
/**
* Lightweight service that bridges the favorites/recently-viewed routed
* components (inside the router-outlet) with the WorkspaceShellComponent
* (the outer shell that renders the context panel aside).
*
* The routed component pushes its category list here via an effect;
* the workspace shell reads it to populate the context panel.
*/
@Injectable({ providedIn: 'root' })
export class FavoritesContextService {
/** Category list managed by the active routed component (Favorites / RecentlyViewed). */
readonly categories = signal<XtreamCategory[]>([]);
/** Currently selected category id — shared between panel and content. */
readonly selectedCategoryId = signal<string>('all');
/** Called by the context panel when the user clicks a category. */
setCategoryId(id: string): void {
this.selectedCategoryId.set(id);
}
/** Called by the routed component to push its category list into the panel. */
setCategories(categories: XtreamCategory[]): void {
this.categories.set(categories);
}
/** Reset to defaults when navigating away from favorites/recent sections. */
reset(): void {
this.categories.set([]);
this.selectedCategoryId.set('all');
}
}
export { FavoritesContextService } from '@iptvnator/portal/shared/util';
@@ -29,8 +29,8 @@ import {
import { EpgViewComponent } from 'shared-portals';
import { PlayerService } from '../../services/player.service';
import { SettingsStore } from '../../services/settings-store.service';
import { XtreamApiService } from '../../xtream-electron/services/xtream-api.service';
import { XtreamUrlService } from '../../xtream-electron/services/xtream-url.service';
import { XtreamApiService } from '@iptvnator/portal/xtream/data-access';
import { XtreamUrlService } from '@iptvnator/portal/xtream/data-access';
import { GlobalFavoritesListComponent } from './global-favorites-list.component';
import { GlobalFavoritesService } from './global-favorites.service';
import { UnifiedFavoriteChannel } from './unified-favorite-channel.interface';
@@ -5,14 +5,14 @@ import { MatDialog } from '@angular/material/dialog';
import { MatIcon } from '@angular/material/icon';
import { MatTooltip } from '@angular/material/tooltip';
import { Router } from '@angular/router';
import { CategoryViewComponent } from '../shared/components/category-view/category-view.component';
import { PlaylistErrorViewComponent } from '../shared/components/playlist-error-view/playlist-error-view.component';
import { StalkerStore } from '../stalker/stalker.store';
import { CategoryViewComponent } from '@iptvnator/portal/shared/ui';
import { PlaylistErrorViewComponent } from '@iptvnator/portal/shared/ui';
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
import {
CategoryManagementDialogComponent,
CategoryManagementDialogData,
} from '../xtream-electron/category-management-dialog/category-management-dialog.component';
import { XtreamStore } from '../xtream-electron/stores/xtream.store';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
type WorkspaceProvider = 'xtreams' | 'stalker' | 'playlists';
@@ -1,5 +1,5 @@
import { Component, computed, inject } from '@angular/core';
import { CategoryViewComponent } from '../shared/components/category-view/category-view.component';
import { CategoryViewComponent } from '@iptvnator/portal/shared/ui';
import { FavoritesContextService } from './favorites-context.service';
/**
@@ -8,7 +8,7 @@ import { PlaylistsService } from 'services';
import { DownloadsService } from '../services/downloads.service';
import { ExternalPlaybackService } from '../services/external-playback.service';
import { SettingsStore } from '../services/settings-store.service';
import { XtreamStore } from '../xtream-electron/stores/xtream.store';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import { FavoritesContextService } from './favorites-context.service';
import { SettingsContextService } from './settings-context.service';
import { WorkspaceShellComponent } from './workspace-shell.component';
@@ -43,12 +43,12 @@ import { ExternalPlaybackDockComponent } from '../shared/components/external-pla
import {
buildPortalRailLinks,
PortalRailLink,
} from '../shared/navigation/portal-rail-links';
import { PortalRailLinksComponent } from '../shared/navigation/portal-rail-links.component';
} from '@iptvnator/portal/shared/util';
import { PortalRailLinksComponent } from '@iptvnator/portal/shared/ui';
import { AccountInfoComponent } from '../xtream-electron/account-info/account-info.component';
import { GlobalRecentlyViewedComponent } from '../xtream-electron/recently-viewed/global-recently-viewed.component';
import { GlobalSearchResultsComponent } from '../xtream-electron/search-results/global-search-results.component';
import { XtreamStore } from '../xtream-electron/stores/xtream.store';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import { FavoritesContextService } from './favorites-context.service';
import { SettingsContextService } from './settings-context.service';
import {
@@ -5,9 +5,9 @@ import { MatListModule } from '@angular/material/list';
import { Store } from '@ngrx/store';
import { TranslatePipe } from '@ngx-translate/core';
import { selectActivePlaylist } from 'm3u-state';
import { XtreamApiService } from '../services/xtream-api.service';
import { XtreamAccountInfo } from './account-info.interface';
import { createLogger } from '../../shared/utils/logger';
import { XtreamApiService } from '@iptvnator/portal/xtream/data-access';
import { XtreamAccountInfo } from '@iptvnator/portal/xtream/data-access';
import { createLogger } from '@iptvnator/portal/shared/util';
@Component({
selector: 'app-account-info',
@@ -14,7 +14,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSnackBar } from '@angular/material/snack-bar';
import { TranslatePipe } from '@ngx-translate/core';
import { DatabaseService, XCategoryFromDb } from 'services';
import { createLogger } from '../../shared/utils/logger';
import { createLogger } from '@iptvnator/portal/shared/util';
export interface CategoryManagementDialogData {
playlistId: string;
@@ -16,23 +16,23 @@ import { TranslatePipe } from '@ngx-translate/core';
import { selectActivePlaylist } from 'm3u-state';
import { BehaviorSubject, switchMap } from 'rxjs';
import { PlayerService } from '../../services/player.service';
import { PortalCollectionLiveShellComponent } from '../../shared/components/portal-collection-live-shell/portal-collection-live-shell.component';
import { PortalCollectionLiveShellComponent } from '@iptvnator/portal/shared/ui';
import {
PortalCollectionMode,
PortalCollectionShellComponent,
PortalCollectionShellLayout,
} from '../../shared/components/portal-collection-shell/portal-collection-shell.component';
import { queryParamSignal } from '../../shared/navigation/portal-route.utils';
import { createPortalCollectionContext } from '../../shared/utils/portal-collection-context';
} from '@iptvnator/portal/shared/ui';
import { queryParamSignal } from '@iptvnator/portal/shared/util';
import { createPortalCollectionContext } from '@iptvnator/portal/shared/util';
import {
buildStandardCollectionCategories,
filterCollectionBucket,
} from '../../shared/utils/portal-collection-items';
import { FavoritesContextService } from '../../workspace/favorites-context.service';
} from '@iptvnator/portal/shared/util';
import { FavoritesContextService } from '@iptvnator/portal/shared/util';
import { PortalChannelsListComponent } from '../portal-channels-list/portal-channels-list.component';
import { FavoriteItem } from '../services/favorite-item.interface';
import { FavoritesService } from '../services/favorites.service';
import { XtreamStore } from '../stores/xtream.store';
import { FavoriteItem } from '@iptvnator/portal/xtream/data-access';
import { FavoritesService } from '@iptvnator/portal/xtream/data-access';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
const XTREAM_FAVORITES_LAYOUT: PortalCollectionShellLayout = {};
const XTREAM_COLLECTION_LABELS = {
@@ -27,9 +27,9 @@ import { PlaylistSwitcherComponent, ResizableDirective } from 'components';
import { XtreamCategory } from 'shared-interfaces';
import { EpgViewComponent, WebPlayerViewComponent } from 'shared-portals';
import { SettingsStore } from '../../services/settings-store.service';
import { CategoryViewComponent } from '../../shared/components/category-view/category-view.component';
import { PortalEmptyStateComponent } from '../../shared/components/portal-empty-state/portal-empty-state.component';
import { isWorkspaceLayoutRoute } from '../../shared/navigation/portal-route.utils';
import { CategoryViewComponent } from '@iptvnator/portal/shared/ui';
import { PortalEmptyStateComponent } from '@iptvnator/portal/shared/ui';
import { isWorkspaceLayoutRoute } from '@iptvnator/portal/shared/util';
import {
getAdjacentChannelItem,
getChannelItemByNumber,
@@ -39,8 +39,8 @@ import {
CategoryManagementDialogData,
} from '../category-management-dialog/category-management-dialog.component';
import { PortalChannelsListComponent } from '../portal-channels-list/portal-channels-list.component';
import { FavoritesService } from '../services/favorites.service';
import { XtreamStore } from '../stores/xtream.store';
import { FavoritesService } from '@iptvnator/portal/xtream/data-access';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
type LiveChannelSortMode = 'server' | 'name-asc' | 'name-desc';
const LIVE_CHANNEL_SORT_STORAGE_KEY = 'xtream-live-channel-sort-mode';
@@ -8,7 +8,7 @@ import { MatTooltip } from '@angular/material/tooltip';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { PlayerContentInfo } from 'shared-interfaces';
import { WebPlayerViewComponent } from 'shared-portals';
import { XtreamStore } from '../stores/xtream.store';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
export interface PlayerDialogData {
streamUrl: string;
@@ -29,10 +29,10 @@ import {
XtreamItem,
} from 'shared-interfaces';
import { ChannelListItemComponent } from 'components';
import { EpgQueueService } from '../services/epg-queue.service';
import { XtreamCredentials } from '../services/xtream-api.service';
import { FavoritesService } from '../services/favorites.service';
import { XtreamStore } from '../stores/xtream.store';
import { EpgQueueService } from '@iptvnator/portal/xtream/data-access';
import { XtreamCredentials } from '@iptvnator/portal/xtream/data-access';
import { FavoritesService } from '@iptvnator/portal/xtream/data-access';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
type LiveChannelSortMode = 'server' | 'name-asc' | 'name-desc';
@@ -1,9 +1,9 @@
import { Component, computed, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslatePipe } from '@ngx-translate/core';
import { ContentCardComponent } from '../../shared/components/content-card/content-card.component';
import { XtreamStore } from '../stores/xtream.store';
import { ContentType } from '../xtream-state';
import { ContentCardComponent } from '@iptvnator/portal/shared/ui';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import { ContentType } from '@iptvnator/portal/xtream/data-access';
@Component({
selector: 'app-recently-added',
@@ -1,6 +1,6 @@
import { Component } from '@angular/core';
import { RecentlyViewedComponent } from './recently-viewed.component';
import { XtreamStore } from '../stores/xtream.store';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
@Component({
selector: 'app-global-recently-viewed',
@@ -21,28 +21,28 @@ import groupBy from 'lodash/groupBy';
import { firstValueFrom } from 'rxjs';
import { DatabaseService, PlaylistsService } from 'services';
import { PlayerService } from '../../services/player.service';
import { ContentCardComponent } from '../../shared/components/content-card/content-card.component';
import { ContentCardComponent } from '@iptvnator/portal/shared/ui';
import {
PortalCollectionLiveShellComponent,
} from '../../shared/components/portal-collection-live-shell/portal-collection-live-shell.component';
} from '@iptvnator/portal/shared/ui';
import {
PortalCollectionMode,
PortalCollectionShellComponent,
PortalCollectionShellLayout,
} from '../../shared/components/portal-collection-shell/portal-collection-shell.component';
} from '@iptvnator/portal/shared/ui';
import {
isWorkspaceLayoutRoute,
queryParamSignal,
} from '../../shared/navigation/portal-route.utils';
import { createPortalCollectionContext } from '../../shared/utils/portal-collection-context';
} from '@iptvnator/portal/shared/util';
import { createPortalCollectionContext } from '@iptvnator/portal/shared/util';
import {
buildStandardCollectionCategories,
filterCollectionBucket,
} from '../../shared/utils/portal-collection-items';
import { createLogger } from '../../shared/utils/logger';
import { FavoritesContextService } from '../../workspace/favorites-context.service';
} from '@iptvnator/portal/shared/util';
import { createLogger } from '@iptvnator/portal/shared/util';
import { FavoritesContextService } from '@iptvnator/portal/shared/util';
import { PortalChannelsListComponent } from '../portal-channels-list/portal-channels-list.component';
import { XtreamStore } from '../stores/xtream.store';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
const XTREAM_RECENT_LAYOUT: Omit<
PortalCollectionShellLayout,
@@ -1,6 +1,6 @@
import { Component } from '@angular/core';
import { SearchResultsComponent } from './search-results.component';
import { XtreamStore } from '../stores/xtream.store';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
@Component({
selector: 'app-global-search-results',
@@ -4,7 +4,7 @@ import { ActivatedRoute, Router, convertToParamMap } from '@angular/router';
import { of } from 'rxjs';
import { DatabaseService } from 'services';
import { SearchResultsComponent } from './search-results.component';
import { XtreamStore } from '../stores/xtream.store';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
class MockXtreamStore {
readonly searchTerm = signal('');
@@ -23,17 +23,17 @@ import { ActivatedRoute, Router } from '@angular/router';
import { TranslatePipe } from '@ngx-translate/core';
import groupBy from 'lodash/groupBy';
import { DatabaseService } from 'services';
import { ContentCardComponent } from '../../shared/components/content-card/content-card.component';
import { SearchLayoutComponent } from '../../shared/components/search-layout/search-layout.component';
import { ContentCardComponent } from '@iptvnator/portal/shared/ui';
import { SearchLayoutComponent } from '@iptvnator/portal/shared/ui';
import {
isWorkspaceLayoutRoute,
queryParamSignal,
} from '../../shared/navigation/portal-route.utils';
import { createLogger } from '../../shared/utils/logger';
import { XtreamContentItem } from '../data-sources/xtream-data-source.interface';
import { SearchFilters } from '../stores/features/with-search.feature';
import { XtreamStore } from '../stores/xtream.store';
import { ContentType } from '../xtream-state';
} from '@iptvnator/portal/shared/util';
import { createLogger } from '@iptvnator/portal/shared/util';
import { XtreamContentItem } from '@iptvnator/portal/xtream/data-access';
import { SearchFilters } from '@iptvnator/portal/xtream/data-access';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import { ContentType } from '@iptvnator/portal/xtream/data-access';
interface SearchResultsData {
isGlobalSearch: boolean;
@@ -7,12 +7,12 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { XtreamSerieEpisode, XtreamSerieEpisodeInfo } from 'shared-interfaces';
import { XtreamStore } from '../stores/xtream.store';
import { StalkerStore } from '../../stalker/stalker.store';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
import { ProgressCapsuleComponent } from '../shared/progress-capsule/progress-capsule.component';
import { WatchedBadgeComponent } from '../shared/watched-badge/watched-badge.component';
import { DownloadsService } from '../../services/downloads.service';
import { createLogger } from '../../shared/utils/logger';
import { createLogger } from '@iptvnator/portal/shared/util';
type EpisodeViewMode = 'grid' | 'list';
const EPISODE_VIEW_MODE_KEY = 'iptvnator_episode_view_mode';
@@ -14,7 +14,7 @@ import { PlayerService } from '../../services/player.service';
import { ExternalPlaybackService } from '../../services/external-playback.service';
import { PortalInlinePlayerComponent } from '../../shared/components/portal-inline-player/portal-inline-player.component';
import { SeasonContainerComponent } from '../season-container/season-container.component';
import { XtreamStore } from '../stores/xtream.store';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
@Component({
selector: 'app-serial-details',
@@ -1,2 +0,0 @@
export * from './xtream-api.service';
export * from './xtream-url.service';
@@ -23,9 +23,9 @@ import { ExternalPlaybackService } from '../../services/external-playback.servic
import { PlayerService } from '../../services/player.service';
import { SettingsStore } from '../../services/settings-store.service';
import { PortalInlinePlayerComponent } from '../../shared/components/portal-inline-player/portal-inline-player.component';
import { XtreamStore } from '../stores/xtream.store';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import { SafePipe } from '@iptvnator/pipes';
import { createLogger } from '../../shared/utils/logger';
import { createLogger } from '@iptvnator/portal/shared/util';
/**
* Route-based VOD details container for Xtream.
@@ -19,7 +19,7 @@ import {
import { DownloadsService } from '../../services/downloads.service';
import { ExternalPlaybackService } from '../../services/external-playback.service';
import { SafePipe } from '@iptvnator/pipes';
import { StalkerStore } from '../../stalker/stalker.store';
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
import { PortalInlinePlayerComponent } from '../../shared/components/portal-inline-player/portal-inline-player.component';
/**
@@ -11,14 +11,14 @@ import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { PlaylistSwitcherComponent, ResizableDirective } from 'components';
import { XtreamCategory } from 'shared-interfaces';
import { CategoryViewComponent } from '../shared/components/category-view/category-view.component';
import { isWorkspaceLayoutRoute } from '../shared/navigation/portal-route.utils';
import { CategoryViewComponent } from '@iptvnator/portal/shared/ui';
import { isWorkspaceLayoutRoute } from '@iptvnator/portal/shared/util';
import {
CategoryManagementDialogComponent,
CategoryManagementDialogData,
} from './category-management-dialog/category-management-dialog.component';
import { XtreamStore } from './stores/xtream.store';
import { XtreamCategorySortMode } from './stores/features/with-selection.feature';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import { XtreamCategorySortMode } from '@iptvnator/portal/xtream/data-access';
const XTREAM_CATEGORY_SORT_STORAGE_KEY = 'xtream-category-sort-mode';
@@ -10,14 +10,14 @@ import { Store } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core';
import { PlaylistActions } from 'm3u-state';
import { filter } from 'rxjs';
import { PortalRailSection } from '../../shared/navigation/portal-rail-links';
import { PortalRailSection } from '@iptvnator/portal/shared/util';
import {
isWorkspaceLayoutRoute,
resolveCurrentPortalSection,
} from '../../shared/navigation/portal-route.utils';
} from '@iptvnator/portal/shared/util';
import { LoadingOverlayComponent } from '../loading-overlay/loading-overlay.component';
import { NavigationComponent } from '../navigation/navigation.component';
import { XtreamStore } from '../stores/xtream.store';
import { NavigationComponent } from '../../portal-shared/navigation/navigation.component';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
@Component({
templateUrl: './xtream-shell.component.html',
@@ -40,14 +40,14 @@ export const xtreamRoutes: Route[] = [
path: '',
loadComponent: () =>
import(
'./category-content-view/category-content-view.component'
'../portal-shared/category-content-view/category-content-view.component'
).then((c) => c.CategoryContentViewComponent),
},
{
path: ':categoryId',
loadComponent: () =>
import(
'./category-content-view/category-content-view.component'
'../portal-shared/category-content-view/category-content-view.component'
).then((c) => c.CategoryContentViewComponent),
},
{
@@ -70,14 +70,14 @@ export const xtreamRoutes: Route[] = [
path: '',
loadComponent: () =>
import(
'./category-content-view/category-content-view.component'
'../portal-shared/category-content-view/category-content-view.component'
).then((c) => c.CategoryContentViewComponent),
},
{
path: ':categoryId',
loadComponent: () =>
import(
'./category-content-view/category-content-view.component'
'../portal-shared/category-content-view/category-content-view.component'
).then((c) => c.CategoryContentViewComponent),
},
{
@@ -120,7 +120,7 @@ export const xtreamRoutes: Route[] = [
{
path: 'downloads',
loadComponent: () =>
import('./downloads/downloads.component').then(
import('@iptvnator/portal/downloads/feature').then(
(c) => c.DownloadsComponent
),
},
+74 -2
View File
@@ -17,8 +17,80 @@ export default [
allow: ['^.*/eslint(\\.base)?\\.config\\.[cm]?[jt]s$'],
depConstraints: [
{
sourceTag: '*',
onlyDependOnLibsWithTags: ['*'],
sourceTag: 'type:app',
onlyDependOnLibsWithTags: [
'type:feature',
'type:ui',
'type:data-access',
'type:util',
],
},
{
sourceTag: 'type:feature',
onlyDependOnLibsWithTags: [
'type:feature',
'type:ui',
'type:data-access',
'type:util',
],
},
{
sourceTag: 'type:ui',
onlyDependOnLibsWithTags: [
'type:ui',
'type:data-access',
'type:util',
],
},
{
sourceTag: 'type:data-access',
onlyDependOnLibsWithTags: [
'type:data-access',
'type:util',
],
},
{
sourceTag: 'type:util',
onlyDependOnLibsWithTags: ['type:util'],
},
{
sourceTag: 'domain:portal-shared',
onlyDependOnLibsWithTags: [
'domain:portal-shared',
'domain:m3u',
'domain:playback',
'scope:shared',
'scope:portal',
],
},
{
sourceTag: 'domain:xtream',
onlyDependOnLibsWithTags: [
'domain:xtream',
'domain:portal-shared',
'domain:m3u',
'scope:shared',
],
},
{
sourceTag: 'domain:stalker',
onlyDependOnLibsWithTags: [
'domain:stalker',
'domain:portal-shared',
'domain:m3u',
'scope:shared',
],
},
{
sourceTag: 'domain:workspace',
onlyDependOnLibsWithTags: [
'domain:workspace',
'domain:portal-shared',
'domain:xtream',
'domain:stalker',
'domain:m3u',
'scope:shared',
],
},
],
},
+1 -1
View File
@@ -4,7 +4,7 @@
"sourceRoot": "libs/m3u-state/src",
"prefix": "lib",
"projectType": "library",
"tags": [],
"tags": ["scope:playlist", "domain:m3u", "type:data-access"],
"targets": {
"test": {
"executor": "@nx/jest:jest",
@@ -0,0 +1,13 @@
{
"name": "portal-downloads-feature",
"$schema": "../../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/portal/downloads/feature/src",
"prefix": "lib",
"projectType": "library",
"tags": ["scope:portal", "domain:portal-shared", "type:feature"],
"targets": {
"lint": {
"executor": "@nx/eslint:lint"
}
}
}
@@ -0,0 +1 @@
export * from './lib/downloads.component';
@@ -14,22 +14,23 @@ import { ActivatedRoute, Router } from '@angular/router';
import { TranslatePipe } from '@ngx-translate/core';
import { ResizableDirective } from 'components';
import { firstValueFrom, map } from 'rxjs';
import { DatabaseService, PlaylistsService } from 'services';
import {
DownloadItem,
DatabaseService,
type DownloadItem,
DownloadsService,
} from '../../services/downloads.service';
import { CategoryViewComponent } from '../../shared/components/category-view/category-view.component';
PlaylistsService,
} from 'services';
import { CategoryViewComponent } from '@iptvnator/portal/shared/ui';
import {
isWorkspaceLayoutRoute,
queryParamSignal,
} from '../../shared/navigation/portal-route.utils';
import { createPortalCollectionContext } from '../../shared/utils/portal-collection-context';
} from '@iptvnator/portal/shared/util';
import { createPortalCollectionContext } from '@iptvnator/portal/shared/util';
import {
buildStandardCollectionCategories,
filterCollectionBucket,
} from '../../shared/utils/portal-collection-items';
import { FavoritesContextService } from '../../workspace/favorites-context.service';
} from '@iptvnator/portal/shared/util';
import { FavoritesContextService } from '@iptvnator/portal/shared/util';
type PortalSource = 'xtream' | 'stalker';
const DOWNLOAD_COLLECTION_LABELS = {
@@ -44,7 +45,7 @@ const DOWNLOAD_COLLECTION_LABELS = {
templateUrl: './downloads.component.html',
styleUrls: [
'./downloads.component.scss',
'../../shared/styles/portal-sidebar.scss',
'../../../../shared/ui/src/lib/styles/portal-sidebar.scss',
],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
@@ -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"]
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "portal-shared-ui",
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/portal/shared/ui/src",
"prefix": "lib",
"projectType": "library",
"tags": ["scope:portal", "domain:portal-shared", "type:ui"],
"targets": {
"lint": {
"executor": "@nx/eslint:lint"
}
}
}
+10
View File
@@ -0,0 +1,10 @@
export * from './lib/components/category-view/category-view.component';
export * from './lib/components/content-card/content-card.component';
export * from './lib/components/favorites-layout/favorites-layout.component';
export * from './lib/components/playlist-error-view/playlist-error-view.component';
export * from './lib/components/portal-collection-live-shell/portal-collection-live-shell.component';
export * from './lib/components/portal-collection-shell/portal-collection-shell.component';
export * from './lib/components/portal-empty-state/portal-empty-state.component';
export * from './lib/components/search-form/search-form.component';
export * from './lib/components/search-layout/search-layout.component';
export * from './lib/navigation/portal-rail-links.component';
@@ -1,4 +1,4 @@
@use '../../../../nav-list.scss';
@use '../../../../../../../../apps/web/src/nav-list.scss';
:host {
display: block;
@@ -1,5 +1,5 @@
@use '@angular/material' as mat;
@use '../../../shared/styles/content-grid' as grid;
@use '../../styles/content-grid' as grid;
:host {
display: flex;
@@ -5,7 +5,7 @@ import { MatTooltip } from '@angular/material/tooltip';
import { ActivatedRoute } from '@angular/router';
import { TranslatePipe } from '@ngx-translate/core';
import { PlaylistSwitcherComponent, ResizableDirective } from 'components';
import { isWorkspaceLayoutRoute } from '../../navigation/portal-route.utils';
import { isWorkspaceLayoutRoute } from '@iptvnator/portal/shared/util';
import { CategoryViewComponent } from '../category-view/category-view.component';
import { ContentCardComponent } from '../content-card/content-card.component';
@@ -8,7 +8,7 @@ import {
RouterLink,
RouterLinkActive,
} from '@angular/router';
import { PortalRailLink, PortalRailSection } from './portal-rail-links';
import { PortalRailLink, PortalRailSection } from '@iptvnator/portal/shared/util';
@Component({
selector: 'app-portal-rail-links',
@@ -0,0 +1,3 @@
// Canonical source: libs/ui/styles/_content-grid.scss
// This stub forwards the mixins for components that @use this file directly.
@forward '../../../../../../../libs/ui/styles/content-grid';
@@ -0,0 +1,3 @@
// Canonical source: libs/ui/styles/_portal-layout.scss
// This stub forwards the mixin for components that @use this file directly.
@forward '../../../../../../../libs/ui/styles/portal-layout';
@@ -0,0 +1,3 @@
// Canonical source: libs/ui/styles/_portal-sidebar.scss
// This stub forwards the rules for components that @use this file directly.
@forward '../../../../../../../libs/ui/styles/portal-sidebar';
+28
View File
@@ -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"
}
]
}
+12
View File
@@ -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"]
}
Loaded 100 of 198 files, more files were not shown because too many files have changed in this diff. Show more