refactor: update nx monorepo structure

This commit is contained in:
4gray committed 2026-03-09 23:24:34 +01:00
1 parent aa739f43e0
commit 7ac2fd66b8
175 files changed
+3118 -1076

No files matched your search

+27 -22
View File
@@ -5,14 +5,11 @@ import { Router, RouterOutlet } from '@angular/router';
import { Actions, ofType } from '@ngrx/effects';
import { Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core';
import {
GlobalRecentlyViewedComponent,
GlobalSearchResultsComponent,
} from '@iptvnator/portal/xtream/feature';
import { EpgProgressPanelComponent } from 'components';
import { EpgService } from '@iptvnator/epg/data-access';
import { EpgProgressPanelComponent } from '@iptvnator/ui/epg';
import { PlaylistActions, selectAllPlaylistsMeta } from 'm3u-state';
import { filter, take } from 'rxjs';
import { DataService, EpgService } from 'services';
import { DataService } from 'services';
import {
AUTO_UPDATE_PLAYLISTS,
Language,
@@ -149,25 +146,33 @@ export class AppComponent implements OnInit {
}
openGlobalSearch(initialQuery = ''): void {
this.dialog.open(GlobalSearchResultsComponent, {
width: '100%',
height: '100%',
maxWidth: '100%',
panelClass: 'global-search-overlay',
data: { isGlobalSearch: true, initialQuery },
});
void import('@iptvnator/portal/xtream/feature').then(
({ GlobalSearchResultsComponent }) => {
this.dialog.open(GlobalSearchResultsComponent, {
width: '100%',
height: '100%',
maxWidth: '100%',
panelClass: 'global-search-overlay',
data: { isGlobalSearch: true, initialQuery },
});
}
);
}
openGlobalRecent(): void {
this.dialog.open(GlobalRecentlyViewedComponent, {
width: '100%',
height: '100%',
maxWidth: '100%',
panelClass: 'global-search-overlay',
data: { isGlobal: true },
hasBackdrop: true,
disableClose: false,
});
void import('@iptvnator/portal/xtream/feature').then(
({ GlobalRecentlyViewedComponent }) => {
this.dialog.open(GlobalRecentlyViewedComponent, {
width: '100%',
height: '100%',
maxWidth: '100%',
panelClass: 'global-search-overlay',
data: { isGlobal: true },
hasBackdrop: true,
disableClose: false,
});
}
);
}
/**
+1 -1
View File
@@ -24,7 +24,7 @@ import {
PORTAL_EXTERNAL_PLAYBACK,
PORTAL_PLAYER,
} from '@iptvnator/portal/shared/util';
import { PLAYLIST_PLAYER_ACTIONS } from '@iptvnator/playlist/m3u/feature-player';
import { PLAYLIST_PLAYER_ACTIONS } from '@iptvnator/playlist/shared/util';
import { provideXtreamDataSource } from '@iptvnator/portal/xtream/data-access';
import { DataService } from 'services';
import { dbConfig } from 'shared-interfaces';
+14 -4
View File
@@ -1,6 +1,4 @@
import { Route, Routes } from '@angular/router';
import { createStalkerRoutes } from '@iptvnator/portal/stalker/feature';
import { createXtreamRoutes } from '@iptvnator/portal/xtream/feature';
function withWorkspaceLayout(routes: Route[]): Route[] {
return routes.map((route) => ({
@@ -80,8 +78,20 @@ export const routes: Routes = [
(c) => c.DownloadsComponent
),
},
...withWorkspaceLayout(createXtreamRoutes()),
...withWorkspaceLayout(createStalkerRoutes()),
{
path: '',
loadChildren: () =>
import('@iptvnator/portal/xtream/feature').then((m) =>
withWorkspaceLayout(m.createXtreamRoutes())
),
},
{
path: '',
loadChildren: () =>
import('@iptvnator/portal/stalker/feature').then((m) =>
withWorkspaceLayout(m.createStalkerRoutes())
),
},
{
path: 'settings',
data: { layout: 'workspace' },
@@ -25,10 +25,10 @@ export class ElectronServiceStub {
return this.remote.app.getVersion();
}
sendIpcEvent(_type: string, _payload?: unknown): void {
sendIpcEvent<T = unknown>(_type: string, _payload?: unknown): T {
void _type;
void _payload;
return undefined;
return undefined as T;
}
listenOn(
+36 -18
View File
@@ -108,10 +108,16 @@ export class ElectronService extends DataService {
return AppConfig.version;
}
async sendIpcEvent(type: string, payload?: unknown) {
async sendIpcEvent<T = unknown>(
type: string,
payload?: unknown
): Promise<T> {
if (type === PLAYLIST_PARSE_BY_URL) {
this.fetchM3uPlaylistFromUrl(payload);
} else if (type === PLAYLIST_UPDATE) {
return undefined as T;
}
if (type === PLAYLIST_UPDATE) {
this.updateM3uPlaylistFromFile(
payload as {
id: string;
@@ -120,22 +126,29 @@ export class ElectronService extends DataService {
title: string;
}
);
} else if (type === XTREAM_REQUEST) {
return await this.forwardXtreamRequest(
return undefined as T;
}
if (type === XTREAM_REQUEST) {
return (await this.forwardXtreamRequest(
payload as { url: string; params: Record<string, string> }
);
} else if (type === 'STALKER_REQUEST') {
return this.fetchStalkerData(
)) as T;
}
if (type === 'STALKER_REQUEST') {
return (await this.fetchStalkerData(
payload as {
url: string;
macAddress: string;
params: Record<string, string>;
}
);
} else if (type === 'OPEN_MPV_PLAYER') {
)) as T;
}
if (type === 'OPEN_MPV_PLAYER') {
const data = payload as PlayerLaunchPayload;
try {
return await window.electron.openInMpv(
return (await window.electron.openInMpv(
data.url,
data.title ?? '',
data.thumbnail ?? '',
@@ -145,8 +158,7 @@ export class ElectronService extends DataService {
data.contentInfo,
data.startTime,
data.headers ?? undefined
);
/* thumbnail: data.thumbnail ?? '', */
)) as T;
} catch (error: unknown) {
const errorMessage =
this.getErrorDetails(error)?.message ?? String(error);
@@ -160,10 +172,12 @@ export class ElectronService extends DataService {
console.error('MPV launch error:', error);
throw error;
}
} else if (type === 'OPEN_VLC_PLAYER') {
}
if (type === 'OPEN_VLC_PLAYER') {
const data = payload as PlayerLaunchPayload;
try {
return await window.electron.openInVlc(
return (await window.electron.openInVlc(
data.url,
data.title ?? '',
data.thumbnail ?? '',
@@ -173,7 +187,7 @@ export class ElectronService extends DataService {
data.contentInfo,
data.startTime,
data.headers ?? undefined
);
)) as T;
} catch (error: unknown) {
const errorMessage =
this.getErrorDetails(error)?.message ?? String(error);
@@ -187,7 +201,9 @@ export class ElectronService extends DataService {
console.error('VLC launch error:', error);
throw error;
}
} else if (type === AUTO_UPDATE_PLAYLISTS) {
}
if (type === AUTO_UPDATE_PLAYLISTS) {
const data = payload as Playlist[];
const playlists = await window.electron.autoUpdatePlaylists(data);
this.store.dispatch(
@@ -202,9 +218,11 @@ export class ElectronService extends DataService {
null,
{ duration: 2000 }
);
} else {
console.log('Unknown type', type);
return playlists as T;
}
console.log('Unknown type', type);
return undefined as T;
}
private async fetchStalkerData(payload: {
+19 -16
View File
@@ -1,10 +1,6 @@
import { Injectable, inject } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { ExternalPlayerInfoDialogComponent } from 'components';
import {
PlayerDialogComponent,
PlayerDialogData,
} from '@iptvnator/portal/xtream/feature';
import { ExternalPlayerInfoDialogComponent } from '@iptvnator/ui/playback';
import { DataService } from 'services';
import {
ExternalPlayerSession,
@@ -85,7 +81,9 @@ export class PlayerService {
if (!hideExternalInfoDialog) {
this.dialog.open(ExternalPlayerInfoDialogComponent);
}
return await this.dataService.sendIpcEvent(OPEN_MPV_PLAYER, {
return await this.dataService.sendIpcEvent<ExternalPlayerSession>(
OPEN_MPV_PLAYER,
{
url: streamUrl,
title,
thumbnail,
@@ -95,12 +93,15 @@ export class PlayerService {
headers,
contentInfo,
startTime,
});
}
);
} else if (player === VideoPlayer.VLC) {
if (!hideExternalInfoDialog) {
this.dialog.open(ExternalPlayerInfoDialogComponent);
}
return await this.dataService.sendIpcEvent(OPEN_VLC_PLAYER, {
return await this.dataService.sendIpcEvent<ExternalPlayerSession>(
OPEN_VLC_PLAYER,
{
url: streamUrl,
title,
thumbnail,
@@ -110,16 +111,18 @@ export class PlayerService {
headers,
contentInfo,
startTime,
});
}
);
}
this.dialog.open<PlayerDialogComponent, PlayerDialogData>(
PlayerDialogComponent,
{
data: { streamUrl, title, contentInfo, startTime },
width: '80%',
maxWidth: '1200px',
maxHeight: '90vh',
void import('@iptvnator/portal/xtream/feature').then(
({ PlayerDialogComponent }) => {
this.dialog.open(PlayerDialogComponent, {
data: { streamUrl, title, contentInfo, startTime },
width: '80%',
maxWidth: '1200px',
maxHeight: '90vh',
});
}
);
}
@@ -1,11 +1,10 @@
import { inject, Injectable, Provider } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { PlaylistInfoComponent } from 'components';
import { PlaylistInfoComponent } from '@iptvnator/playlist/shared/ui';
import {
PORTAL_NAVIGATION_ACTIONS,
PortalNavigationActions,
} from '@iptvnator/portal/shared/util';
import { AccountInfoComponent } from '@iptvnator/portal/xtream/feature';
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
import { Playlist } from 'shared-interfaces';
import { SettingsComponent } from '../settings/settings.component';
@@ -18,16 +17,22 @@ export class AppPortalNavigationActionsService
private readonly xtreamStore = inject(XtreamStore);
openAccountInfo(): void {
this.dialog.open(AccountInfoComponent, {
width: '80%',
maxWidth: '1200px',
maxHeight: '90vh',
data: {
vodStreamsCount: this.xtreamStore.vodStreams().length,
liveStreamsCount: this.xtreamStore.liveStreams().length,
seriesCount: this.xtreamStore.serialStreams().length,
},
});
const data = {
vodStreamsCount: this.xtreamStore.vodStreams().length,
liveStreamsCount: this.xtreamStore.liveStreams().length,
seriesCount: this.xtreamStore.serialStreams().length,
};
void import('@iptvnator/portal/xtream/feature').then(
({ AccountInfoComponent }) => {
this.dialog.open(AccountInfoComponent, {
width: '80%',
maxWidth: '1200px',
maxHeight: '90vh',
data,
});
}
);
}
openPlaylistInfo(playlist: Playlist | null | undefined): void {
+16 -8
View File
@@ -96,26 +96,34 @@ export class PwaService extends DataService {
* @param type ipc command type
* @param payload payload
*/
sendIpcEvent(type: string, payload?: unknown) {
sendIpcEvent<T = unknown>(type: string, payload?: unknown): T {
if (type === PLAYLIST_PARSE_BY_URL) {
this.fetchFromUrl(payload);
} else if (type === PLAYLIST_UPDATE) {
return undefined as T;
}
if (type === PLAYLIST_UPDATE) {
this.refreshPlaylist(payload);
} else if (type === XTREAM_REQUEST) {
return undefined as T;
}
if (type === XTREAM_REQUEST) {
return this.forwardXtreamRequest(
payload as { url: string; params: Record<string, string> }
);
} else if (type === STALKER_REQUEST) {
) as T;
}
if (type === STALKER_REQUEST) {
return this.forwardStalkerRequest(
payload as {
url: string;
macAddress: string;
params: Record<string, string>;
}
);
} else {
return Promise.resolve();
) as T;
}
return undefined as T;
}
refreshPlaylist(payload: Partial<Playlist & { id: string }>) {
+56 -28
View File
@@ -2,9 +2,58 @@ import { HttpClient } from '@angular/common/http';
import { inject, Injectable } from '@angular/core';
import { StorageMap } from '@ngx-pwa/local-storage';
import { catchError, map, Observable } from 'rxjs';
import * as semver from 'semver';
import { STORE_KEY, Theme } from 'shared-interfaces';
const PRERELEASE_KEYWORDS = [
'beta',
'alpha',
'rc',
'preview',
'dev',
'canary',
'nightly',
] as const;
interface ParsedVersion {
major: number;
minor: number;
patch: number;
prerelease: boolean;
}
function parseVersion(input: string): ParsedVersion | null {
const normalized = input.trim().replace(/^v/i, '');
const match = normalized.match(
/(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:[-+]?([0-9A-Za-z.-]+))?/
);
if (!match) {
return null;
}
return {
major: Number(match[1] ?? 0),
minor: Number(match[2] ?? 0),
patch: Number(match[3] ?? 0),
prerelease: Boolean(match[4]),
};
}
function hasPrereleaseKeyword(value: string): boolean {
const normalized = value.toLowerCase();
return PRERELEASE_KEYWORDS.some((keyword) => normalized.includes(keyword));
}
function compareVersions(a: ParsedVersion, b: ParsedVersion): number {
if (a.major !== b.major) {
return a.major - b.major;
}
if (a.minor !== b.minor) {
return a.minor - b.minor;
}
return a.patch - b.patch;
}
@Injectable({
providedIn: 'root',
})
@@ -64,34 +113,14 @@ export class SettingsService {
map((response) => {
// Filter out pre-release versions (beta, alpha, rc, etc.)
const stableReleases = response.filter((release) => {
const releaseName = release.name.toLowerCase();
// Check for beta/alpha/rc keywords in the release name
const prereleaseKeywords = [
'beta',
'alpha',
'rc',
'preview',
'dev',
'canary',
'nightly',
];
const hasPrereleaseKeyword = prereleaseKeywords.some(
(keyword) => releaseName.includes(keyword)
);
if (hasPrereleaseKeyword) {
if (hasPrereleaseKeyword(release.name)) {
return false;
}
// Validate version format
const version = semver.valid(
semver.coerce(release.name)
);
const version = parseVersion(release.name);
if (!version) return false;
// Check if version has prerelease tags in semver format
return !semver.prerelease(release.name);
return !version.prerelease;
});
// Sort stable releases by creation date
@@ -121,9 +150,8 @@ export class SettingsService {
currentVersion: string,
latestVersion: string
): boolean {
// Clean and coerce versions to handle invalid formats
const cleanCurrent = semver.coerce(currentVersion);
const cleanLatest = semver.coerce(latestVersion);
const cleanCurrent = parseVersion(currentVersion);
const cleanLatest = parseVersion(latestVersion);
if (!cleanCurrent || !cleanLatest) {
console.warn('Invalid version format:', {
@@ -133,6 +161,6 @@ export class SettingsService {
return false;
}
return semver.lt(cleanCurrent, cleanLatest);
return compareVersions(cleanCurrent, cleanLatest) < 0;
}
}
@@ -1,12 +1,6 @@
import { inject, Injectable, Provider } from '@angular/core';
import { MatDialog } from '@angular/material/dialog';
import { AddPlaylistDialogComponent } from '@iptvnator/playlist/import/feature';
import {
AccountInfoComponent,
GlobalRecentlyViewedComponent,
GlobalSearchResultsComponent,
} from '@iptvnator/portal/xtream/feature';
import { PlaylistType } from 'components';
import { PlaylistType } from '@iptvnator/playlist/shared/ui';
import {
WORKSPACE_SHELL_ACTIONS,
WorkspaceAccountInfoData,
@@ -18,44 +12,60 @@ export class AppWorkspaceShellActionsService implements WorkspaceShellActions {
private readonly dialog = inject(MatDialog);
openAddPlaylistDialog(type: PlaylistType): void {
this.dialog.open(AddPlaylistDialogComponent, {
width: '600px',
data: { type },
});
void import('@iptvnator/playlist/import/feature').then(
({ AddPlaylistDialogComponent }) => {
this.dialog.open(AddPlaylistDialogComponent, {
width: '600px',
data: { type },
});
}
);
}
openGlobalSearch(initialQuery = ''): void {
this.dialog.open(GlobalSearchResultsComponent, {
width: '100%',
height: '100%',
maxWidth: '100%',
panelClass: 'global-search-overlay',
data: {
isGlobalSearch: true,
initialQuery,
},
});
void import('@iptvnator/portal/xtream/feature').then(
({ GlobalSearchResultsComponent }) => {
this.dialog.open(GlobalSearchResultsComponent, {
width: '100%',
height: '100%',
maxWidth: '100%',
panelClass: 'global-search-overlay',
data: {
isGlobalSearch: true,
initialQuery,
},
});
}
);
}
openGlobalRecent(): void {
this.dialog.open(GlobalRecentlyViewedComponent, {
width: '100%',
height: '100%',
maxWidth: '100%',
panelClass: 'global-search-overlay',
data: { isGlobal: true },
hasBackdrop: true,
disableClose: false,
});
void import('@iptvnator/portal/xtream/feature').then(
({ GlobalRecentlyViewedComponent }) => {
this.dialog.open(GlobalRecentlyViewedComponent, {
width: '100%',
height: '100%',
maxWidth: '100%',
panelClass: 'global-search-overlay',
data: { isGlobal: true },
hasBackdrop: true,
disableClose: false,
});
}
);
}
openAccountInfo(data: WorkspaceAccountInfoData): void {
this.dialog.open(AccountInfoComponent, {
width: '80%',
maxWidth: '1200px',
maxHeight: '90vh',
data,
});
void import('@iptvnator/portal/xtream/feature').then(
({ AccountInfoComponent }) => {
this.dialog.open(AccountInfoComponent, {
width: '80%',
maxWidth: '1200px',
maxHeight: '90vh',
data,
});
}
);
}
}
@@ -18,13 +18,14 @@ import { Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { provideMockStore } from '@ngrx/store/testing';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { EpgService } from '@iptvnator/epg/data-access';
import {
MockModule,
MockProvider,
MockProviders,
} from 'ng-mocks';
import { DialogService } from 'components';
import { DataService, EpgService, PlaylistsService } from 'services';
import { DataService, PlaylistsService } from 'services';
import { Language, StreamFormat, Theme, VideoPlayer } from 'shared-interfaces';
import { SettingsComponent } from './settings.component';
@@ -34,11 +34,12 @@ import { MatTooltipModule } from '@angular/material/tooltip';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { EpgService } from '@iptvnator/epg/data-access';
import { QRCodeComponent } from 'angularx-qrcode';
import { DialogService } from 'components';
import { PlaylistActions, selectIsEpgAvailable } from 'm3u-state';
import { take } from 'rxjs';
import { DataService, EpgService, PlaylistsService } from 'services';
import { DataService, PlaylistsService } from 'services';
import {
Language,
Playlist,