mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
refactor: rework ipc events handling
This commit is contained in:
1 parent
d3fc2a5af8
commit
656c86dc30
24 files changed
+218
-389
No files matched your search
@@ -8,8 +8,10 @@ contextBridge.exposeInMainWorld('electron', {
|
||||
updatePlaylistFromFilePath: (filePath: string, title: string) =>
|
||||
ipcRenderer.invoke('update-playlist-from-file-path', filePath, title),
|
||||
openPlaylistFromFile: () => ipcRenderer.invoke('open-playlist-from-file'),
|
||||
saveFileDialog: (defaultPath: string, filters?: { name: string; extensions: string[] }[]) =>
|
||||
ipcRenderer.invoke('save-file-dialog', defaultPath, filters),
|
||||
saveFileDialog: (
|
||||
defaultPath: string,
|
||||
filters?: { name: string; extensions: string[] }[]
|
||||
) => ipcRenderer.invoke('save-file-dialog', defaultPath, filters),
|
||||
writeFile: (filePath: string, content: string) =>
|
||||
ipcRenderer.invoke('write-file', filePath, content),
|
||||
setUserAgent: (userAgent: string, referer?: string) =>
|
||||
|
||||
@@ -78,6 +78,8 @@ export default class App {
|
||||
preload: join(__dirname, 'main.preload.js'),
|
||||
},
|
||||
...savedWindowBounds,
|
||||
minHeight: 600,
|
||||
minWidth: 900,
|
||||
});
|
||||
App.mainWindow.setMenu(null);
|
||||
if (!savedWindowBounds) {
|
||||
|
||||
@@ -90,7 +90,10 @@ ipcMain.handle(
|
||||
);
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error('Failed to send command to existing MPV:', err);
|
||||
console.error(
|
||||
'Failed to send command to existing MPV:',
|
||||
err
|
||||
);
|
||||
// If it fails, clear the reference and create a new one
|
||||
mpvProcess = null;
|
||||
mpvSocketPath = null;
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import axios, { AxiosRequestConfig } from 'axios';
|
||||
import { ipcMain } from 'electron';
|
||||
import { XTREAM_REQUEST } from 'shared-interfaces';
|
||||
|
||||
export default class XtreamEvents {
|
||||
static bootstrapXtreamEvents(): Electron.IpcMain {
|
||||
@@ -17,7 +16,7 @@ export default class XtreamEvents {
|
||||
* Handle Xtream Codes API requests
|
||||
*/
|
||||
ipcMain.handle(
|
||||
XTREAM_REQUEST,
|
||||
'XTREAM_REQUEST',
|
||||
async (
|
||||
event,
|
||||
payload: {
|
||||
@@ -77,7 +76,11 @@ ipcMain.handle(
|
||||
status: error.response?.status || 500,
|
||||
};
|
||||
throw errorResponse;
|
||||
} else if (error && typeof error === 'object' && 'message' in error) {
|
||||
} else if (
|
||||
error &&
|
||||
typeof error === 'object' &&
|
||||
'message' in error
|
||||
) {
|
||||
throw error;
|
||||
} else {
|
||||
throw {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Component, inject, OnInit } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router, RouterOutlet } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
@@ -8,15 +7,11 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { DataService, EpgService } from 'services';
|
||||
import {
|
||||
ERROR,
|
||||
IpcCommand,
|
||||
Language,
|
||||
OPEN_FILE,
|
||||
Settings,
|
||||
STORE_KEY,
|
||||
Theme,
|
||||
VIEW_ADD_PLAYLIST,
|
||||
VIEW_SETTINGS,
|
||||
} from 'shared-interfaces';
|
||||
import { SettingsService } from './services/settings.service';
|
||||
import { RecentlyViewedComponent } from './xtream-tauri/recently-viewed/recently-viewed.component';
|
||||
@@ -27,30 +22,24 @@ import { SearchResultsComponent } from './xtream-tauri/search-results/search-res
|
||||
templateUrl: './app.component.html',
|
||||
imports: [RouterOutlet],
|
||||
})
|
||||
export class AppComponent implements OnInit, OnDestroy {
|
||||
export class AppComponent implements OnInit {
|
||||
private dataService = inject(DataService);
|
||||
private dialog = inject(MatDialog);
|
||||
private epgService = inject(EpgService);
|
||||
private router = inject(Router);
|
||||
private store = inject(Store);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private translate = inject(TranslateService);
|
||||
private settingsService = inject(SettingsService);
|
||||
|
||||
/** List of ipc commands with function mapping */
|
||||
private readonly commandsList = [
|
||||
/* private readonly commandsList = [
|
||||
new IpcCommand(VIEW_ADD_PLAYLIST, () => this.navigateToRoute('/')),
|
||||
new IpcCommand(VIEW_SETTINGS, () => this.navigateToRoute('/settings')),
|
||||
new IpcCommand(ERROR, (response: { message: string; status: number }) =>
|
||||
this.showErrorAsNotification(response)
|
||||
),
|
||||
];
|
||||
]; */
|
||||
|
||||
/** Default language as fallback */
|
||||
private readonly DEFAULT_LANG = Language.ENGLISH;
|
||||
|
||||
private listeners = [];
|
||||
|
||||
constructor() {
|
||||
if (
|
||||
((this.dataService.isElectron &&
|
||||
@@ -90,24 +79,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
this.store.dispatch(PlaylistActions.loadPlaylists());
|
||||
this.translate.setDefaultLang(this.DEFAULT_LANG);
|
||||
|
||||
this.setRendererListeners();
|
||||
this.initSettings();
|
||||
/* this.handleWhatsNewDialog(); */
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes all necessary listeners for the events from the renderer process
|
||||
*/
|
||||
setRendererListeners(): void {
|
||||
this.commandsList.forEach((command) => {
|
||||
const cb = (response) => {
|
||||
if (response.data.type === command.id) {
|
||||
command.callback(response.data);
|
||||
}
|
||||
};
|
||||
this.dataService.listenOn(command.id, cb);
|
||||
this.listeners.push(cb);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -134,6 +106,8 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
this.epgService.fetchEpg(settings.epgUrl);
|
||||
}
|
||||
|
||||
// TODO: trigger auto-refresh mechanism for playlists
|
||||
|
||||
if (settings.theme) {
|
||||
this.settingsService.changeTheme(settings.theme);
|
||||
} else {
|
||||
@@ -160,21 +134,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the actual version of the application and shows the "what is new" dialog if the updated version was detected
|
||||
*/
|
||||
/* handleWhatsNewDialog(): void {
|
||||
const actualVersion = this.dataService.getAppVersion();
|
||||
this.settingsService
|
||||
.getValueFromLocalStorage(STORE_KEY.Version)
|
||||
.subscribe(() => {
|
||||
this.settingsService.setValueToLocalStorage(
|
||||
STORE_KEY.Version,
|
||||
actualVersion
|
||||
);
|
||||
});
|
||||
} */
|
||||
|
||||
/**
|
||||
* Navigate to the specified route
|
||||
* @param route route to navigate to
|
||||
@@ -183,16 +142,6 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
this.router.navigateByUrl(route);
|
||||
}
|
||||
|
||||
showErrorAsNotification(response: { message: string; status: number }) {
|
||||
this.snackBar.open(
|
||||
`Error: ${response?.message ?? 'Something went wrong'} (Status: ${
|
||||
response?.status ?? 0
|
||||
})`,
|
||||
null,
|
||||
{ duration: 4000 }
|
||||
);
|
||||
}
|
||||
|
||||
openGlobalSearch(): void {
|
||||
this.dialog.open(SearchResultsComponent, {
|
||||
width: '100%',
|
||||
@@ -214,19 +163,4 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
disableClose: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all ipc command listeners on component destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (this.dataService.isElectron) {
|
||||
this.commandsList.forEach((command) =>
|
||||
this.dataService.removeAllListeners(command.id)
|
||||
);
|
||||
} else {
|
||||
this.listeners.forEach((listener) =>
|
||||
window.removeEventListener('message', listener)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,5 @@
|
||||
<div class="grid-container">
|
||||
<app-header
|
||||
<app-header
|
||||
[title]="'HOME.TITLE' | translate"
|
||||
[subtitle]="'HOME.SUBTITLE' | translate"
|
||||
/>
|
||||
<app-recent-playlists class="recent-playlists" />
|
||||
@if (isLoading) {
|
||||
<mat-progress-bar mode="indeterminate" />
|
||||
}
|
||||
</div>
|
||||
/>
|
||||
<app-recent-playlists class="recent-playlists" />
|
||||
@@ -1,24 +1,4 @@
|
||||
::ng-deep {
|
||||
.mat-tab-label-content {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mat-tab-group.mat-background-primary .mat-tab-header-pagination {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.playlists-container {
|
||||
grid-area: playlists;
|
||||
}
|
||||
|
||||
.recent-playlists {
|
||||
height: calc(100vh - 100px);
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 480px) {
|
||||
.tab-title {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -1,103 +1,12 @@
|
||||
import { Component, inject, OnDestroy } from '@angular/core';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { Component } from '@angular/core';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { RecentPlaylistsComponent } from 'components';
|
||||
import { addPlaylist } from 'm3u-state';
|
||||
import { DataService } from 'services';
|
||||
import { ERROR, Playlist, PLAYLIST_PARSE_RESPONSE } from 'shared-interfaces';
|
||||
import { HeaderComponent } from '../shared/components/header/header.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-home',
|
||||
templateUrl: './home.component.html',
|
||||
styleUrls: ['./home.component.scss'],
|
||||
imports: [
|
||||
HeaderComponent,
|
||||
MatProgressBarModule,
|
||||
RecentPlaylistsComponent,
|
||||
TranslatePipe,
|
||||
],
|
||||
imports: [HeaderComponent, RecentPlaylistsComponent, TranslatePipe],
|
||||
})
|
||||
export class HomeComponent implements OnDestroy {
|
||||
private dataService = inject(DataService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private readonly store = inject(Store);
|
||||
|
||||
/** Loading spinner state */
|
||||
isLoading = false;
|
||||
|
||||
/** IPC Renderer commands list with callbacks */
|
||||
commandsList = [
|
||||
{
|
||||
id: PLAYLIST_PARSE_RESPONSE,
|
||||
execute: (response: { payload: Playlist }): void => {
|
||||
this.store.dispatch(
|
||||
addPlaylist({
|
||||
playlist: response.payload,
|
||||
})
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: ERROR,
|
||||
execute: (error: { message: string; status: string }) => {
|
||||
this.showNotification('Error: ' + error.message);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
listeners = [];
|
||||
|
||||
constructor() {
|
||||
this.setRendererListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set electrons main process listeners
|
||||
*/
|
||||
setRendererListeners(): void {
|
||||
this.commandsList.forEach((command) => {
|
||||
/* if (this.dataService.isElectron) {
|
||||
this.dataService.listenOn(command.id, (event, response) =>
|
||||
this.ngZone.run(() => command.execute(response))
|
||||
);
|
||||
} else { */
|
||||
const cb = (response) => {
|
||||
if (response.data.type === command.id) {
|
||||
command.execute(response.data);
|
||||
}
|
||||
};
|
||||
this.dataService.listenOn(command.id, cb);
|
||||
this.listeners.push(cb);
|
||||
/* } */
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows snack bar notification with a given message
|
||||
* @param message message to show
|
||||
* @param duration visibility duration of the snackbar
|
||||
*/
|
||||
showNotification(message: string, duration = 2000): void {
|
||||
this.snackBar.open(message, null, {
|
||||
duration,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove ipcRenderer listeners after component destroy
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (this.dataService.isElectron) {
|
||||
this.commandsList.forEach((command) =>
|
||||
this.dataService.removeAllListeners(command.id)
|
||||
);
|
||||
} else {
|
||||
this.listeners.forEach((listener) => {
|
||||
window.removeEventListener('message', listener);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
export class HomeComponent {}
|
||||
@@ -96,7 +96,7 @@
|
||||
<!-- channel overlay -->
|
||||
<app-info-overlay
|
||||
[channel]="activeChannel"
|
||||
[epgProgram]="epgProgram$ | async"
|
||||
[epgProgram]="(epgProgram$ | async)!"
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import { Overlay, OverlayRef } from '@angular/cdk/overlay';
|
||||
import { ComponentPortal } from '@angular/cdk/portal';
|
||||
import { AsyncPipe, CommonModule } from '@angular/common';
|
||||
import {
|
||||
Component,
|
||||
Injector,
|
||||
NgZone,
|
||||
OnInit,
|
||||
effect,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { Component, Injector, OnInit, effect, inject } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
@@ -37,10 +29,7 @@ import { DataService, PlaylistsService } from 'services';
|
||||
import {
|
||||
COMPONENT_OVERLAY_REF,
|
||||
Channel,
|
||||
ERROR,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_PARSE_RESPONSE,
|
||||
Playlist,
|
||||
STORE_KEY,
|
||||
Settings,
|
||||
SidebarView,
|
||||
@@ -51,37 +40,35 @@ import { SettingsComponent } from '../../settings/settings.component';
|
||||
|
||||
@Component({
|
||||
imports: [
|
||||
ArtPlayerComponent,
|
||||
AsyncPipe,
|
||||
AudioPlayerComponent,
|
||||
InfoOverlayComponent,
|
||||
CommonModule,
|
||||
EpgListComponent,
|
||||
HtmlVideoPlayerComponent,
|
||||
InfoOverlayComponent,
|
||||
MatSidenavModule,
|
||||
RouterLink,
|
||||
SidebarComponent,
|
||||
ToolbarComponent,
|
||||
VjsPlayerComponent,
|
||||
ArtPlayerComponent,
|
||||
],
|
||||
templateUrl: './video-player.component.html',
|
||||
styleUrl: './video-player.component.scss',
|
||||
})
|
||||
export class VideoPlayerComponent implements OnInit {
|
||||
private activatedRoute = inject(ActivatedRoute);
|
||||
private dataService = inject(DataService);
|
||||
private readonly activatedRoute = inject(ActivatedRoute);
|
||||
private readonly dataService = inject(DataService);
|
||||
private readonly dialog = inject(MatDialog);
|
||||
private ngZone = inject(NgZone);
|
||||
private overlay = inject(Overlay);
|
||||
private playlistsService = inject(PlaylistsService);
|
||||
private router = inject(Router);
|
||||
private settingsStore = inject(SettingsStore);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private storage = inject(StorageMap);
|
||||
private store = inject(Store);
|
||||
private readonly overlay = inject(Overlay);
|
||||
private readonly playlistsService = inject(PlaylistsService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly settingsStore = inject(SettingsStore);
|
||||
private readonly storage = inject(StorageMap);
|
||||
private readonly store = inject(Store);
|
||||
|
||||
/** Active selected channel */
|
||||
activeChannel$ = this.store
|
||||
readonly activeChannel$ = this.store
|
||||
.select(selectActive)
|
||||
.pipe(filter((channel) => Boolean((channel as Channel)?.url)));
|
||||
|
||||
@@ -89,7 +76,7 @@ export class VideoPlayerComponent implements OnInit {
|
||||
channels$!: Observable<Channel[]>;
|
||||
|
||||
/** Current epg program */
|
||||
epgProgram$ = this.store.select(selectCurrentEpgProgram);
|
||||
readonly epgProgram$ = this.store.select(selectCurrentEpgProgram);
|
||||
|
||||
/** Selected video player options */
|
||||
playerSettings: Partial<Settings> = {
|
||||
@@ -97,37 +84,6 @@ export class VideoPlayerComponent implements OnInit {
|
||||
showCaptions: false,
|
||||
};
|
||||
|
||||
/** IPC Renderer commands list with callbacks */
|
||||
commandsList = [
|
||||
{
|
||||
id: ERROR,
|
||||
execute: (response: { message: string }): void => {
|
||||
this.snackBar.open(response.message, '', {
|
||||
duration: 3100,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
id: PLAYLIST_PARSE_RESPONSE,
|
||||
execute: (response: { payload: Playlist }): void => {
|
||||
if (response.payload.isTemporary) {
|
||||
this.store.dispatch(
|
||||
PlaylistActions.setChannels({
|
||||
channels: response.payload.playlist.items,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
this.store.dispatch(
|
||||
PlaylistActions.addPlaylist({
|
||||
playlist: response.payload,
|
||||
})
|
||||
);
|
||||
}
|
||||
this.sidebarView = 'CHANNELS';
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
readonly isDesktop = !!window['electron'];
|
||||
|
||||
sidebarView: SidebarView = 'CHANNELS';
|
||||
@@ -158,7 +114,6 @@ export class VideoPlayerComponent implements OnInit {
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.applySettings();
|
||||
this.setRendererListeners();
|
||||
this.getPlaylistUrlAsParam();
|
||||
|
||||
this.channels$ = this.activatedRoute.params.pipe(
|
||||
@@ -214,23 +169,6 @@ export class VideoPlayerComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
setRendererListeners(): void {
|
||||
this.commandsList.forEach((command) => {
|
||||
if (this.isDesktop) {
|
||||
this.dataService.listenOn(command.id, (event, response) =>
|
||||
this.ngZone.run(() => command.execute(response))
|
||||
);
|
||||
} else {
|
||||
const cb = (response: any) => {
|
||||
if (response.data.type === command.id) {
|
||||
command.execute(response.data);
|
||||
}
|
||||
};
|
||||
this.dataService.listenOn(command.id, cb);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the app configuration from the browsers storage and applies the settings in the current component
|
||||
*/
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Params } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { DataService } from 'services';
|
||||
import {
|
||||
AUTO_UPDATE_PLAYLISTS,
|
||||
AUTO_UPDATE_PLAYLISTS_RESPONSE,
|
||||
ERROR,
|
||||
Playlist,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_PARSE_RESPONSE,
|
||||
PLAYLIST_UPDATE,
|
||||
PLAYLIST_UPDATE_RESPONSE,
|
||||
XTREAM_RESPONSE,
|
||||
} from 'shared-interfaces';
|
||||
import { AppConfig } from '../../environments/environment';
|
||||
@@ -20,7 +20,9 @@ import { AppConfig } from '../../environments/environment';
|
||||
})
|
||||
export class ElectronService extends DataService {
|
||||
private eventListeners: { [key: string]: () => void } = {};
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private readonly snackBar = inject(MatSnackBar);
|
||||
private readonly store = inject(Store);
|
||||
private readonly translateService = inject(TranslateService);
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -95,27 +97,18 @@ export class ElectronService extends DataService {
|
||||
} else if (type === AUTO_UPDATE_PLAYLISTS) {
|
||||
const data = payload as Playlist[];
|
||||
const playlists = await window.electron.autoUpdatePlaylists(data);
|
||||
/* for await (const item of data) {
|
||||
if (item.filePath) {
|
||||
const playlist = await readTextFile(item.filePath);
|
||||
const parsedPlaylist = parse(playlist);
|
||||
const playlistObject = createPlaylistObject(
|
||||
item.title,
|
||||
parsedPlaylist,
|
||||
item.filePath,
|
||||
'FILE'
|
||||
);
|
||||
playlists.push({ ...playlistObject, _id: item._id });
|
||||
}
|
||||
}
|
||||
*/
|
||||
window.postMessage({
|
||||
type: AUTO_UPDATE_PLAYLISTS_RESPONSE,
|
||||
payload: {
|
||||
message: 'Success! The playlists were successfully updated',
|
||||
this.store.dispatch(
|
||||
PlaylistActions.updateManyPlaylists({
|
||||
playlists,
|
||||
},
|
||||
});
|
||||
})
|
||||
);
|
||||
this.snackBar.open(
|
||||
this.translateService.instant(
|
||||
'HOME.PLAYLISTS.AUTO_REFRESH_UPDATE_SUCCESS'
|
||||
),
|
||||
null,
|
||||
{ duration: 2000 }
|
||||
);
|
||||
} else {
|
||||
console.log('Unknown type', type);
|
||||
}
|
||||
@@ -130,25 +123,27 @@ export class ElectronService extends DataService {
|
||||
// Use Electron IPC to make the Stalker request
|
||||
const response = await window.electron.stalkerRequest(payload);
|
||||
return response;
|
||||
} catch (err: any) {
|
||||
} catch (err) {
|
||||
console.error('Stalker request error:', err);
|
||||
window.postMessage({
|
||||
type: ERROR,
|
||||
message: err.message ?? 'Error: not found',
|
||||
status: err.status ?? 404,
|
||||
});
|
||||
this.snackBar.open(
|
||||
`Error: ${err.message ?? ' Not found'}, status: ${err.status ?? 404}`,
|
||||
'Close',
|
||||
{
|
||||
duration: 5000,
|
||||
}
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchM3uPlaylistFromUrl(payload: Partial<Playlist>) {
|
||||
console.log(payload);
|
||||
window.electron.fetchPlaylistByUrl(payload.url).then((result) => {
|
||||
// TODO: call store and decide and store where to store based on the isTemporary flag etc
|
||||
window.postMessage({
|
||||
type: PLAYLIST_PARSE_RESPONSE,
|
||||
payload: { ...result, isTemporary: payload.isTemporary },
|
||||
});
|
||||
this.store.dispatch(
|
||||
PlaylistActions.handleAddingPlaylistByUrl({
|
||||
isTemporary: !!payload?.isTemporary,
|
||||
playlist: result,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -179,16 +174,23 @@ export class ElectronService extends DataService {
|
||||
}
|
||||
|
||||
methodToCall.then((playlistObject) => {
|
||||
window.postMessage({
|
||||
type: PLAYLIST_UPDATE_RESPONSE,
|
||||
payload: {
|
||||
message: 'Success! The playlist was successfully updated',
|
||||
this.store.dispatch(
|
||||
PlaylistActions.updatePlaylist({
|
||||
playlist: {
|
||||
...playlistObject,
|
||||
_id: data.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
playlistId: data.id,
|
||||
})
|
||||
);
|
||||
|
||||
this.snackBar.open(
|
||||
this.translateService.instant(
|
||||
'HOME.PLAYLISTS.PLAYLIST_UPDATE_SUCCESS'
|
||||
),
|
||||
null,
|
||||
{ duration: 2000 }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -224,14 +226,21 @@ export class ElectronService extends DataService {
|
||||
window.postMessage(result);
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
console.error('Xtream request error:', error);
|
||||
const result = {
|
||||
console.error('Xtream request error:', error.message);
|
||||
this.snackBar.open(
|
||||
`Error: ${error.message ?? 'Failed to connect to Xtream server'}, status: ${
|
||||
error.status ?? 500
|
||||
}`,
|
||||
'Close',
|
||||
{
|
||||
duration: 5000,
|
||||
}
|
||||
);
|
||||
return {
|
||||
type: ERROR,
|
||||
status: error.status ?? 500,
|
||||
message: error.message ?? 'Failed to connect to Xtream server',
|
||||
};
|
||||
window.postMessage(result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
ERROR,
|
||||
Playlist,
|
||||
PLAYLIST_PARSE_BY_URL,
|
||||
PLAYLIST_PARSE_RESPONSE,
|
||||
PLAYLIST_UPDATE,
|
||||
STALKER_REQUEST,
|
||||
XTREAM_REQUEST,
|
||||
@@ -88,9 +87,15 @@ export class PwaService extends DataService {
|
||||
this.getPlaylistFromUrl(payload.url)
|
||||
.pipe(
|
||||
catchError((error) => {
|
||||
window.postMessage({
|
||||
type: ERROR,
|
||||
});
|
||||
this.snackBar.open(
|
||||
`Error: ${error.message ?? 'Unknown error'}, status: ${
|
||||
error.status ?? 500
|
||||
}`,
|
||||
'Close',
|
||||
{
|
||||
duration: 5000,
|
||||
}
|
||||
);
|
||||
return throwError(() => error);
|
||||
})
|
||||
)
|
||||
@@ -120,19 +125,23 @@ export class PwaService extends DataService {
|
||||
this.getPlaylistFromUrl(payload.url)
|
||||
.pipe(
|
||||
catchError((error) => {
|
||||
window.postMessage({
|
||||
type: ERROR,
|
||||
message: this.getErrorMessageByStatusCode(error.status),
|
||||
status: error.status,
|
||||
});
|
||||
this.snackBar.open(
|
||||
this.getErrorMessageByStatusCode(error.status),
|
||||
'Close',
|
||||
{
|
||||
duration: 5000,
|
||||
}
|
||||
);
|
||||
return throwError(() => error);
|
||||
})
|
||||
)
|
||||
.subscribe((response: any) => {
|
||||
window.postMessage({
|
||||
type: PLAYLIST_PARSE_RESPONSE,
|
||||
payload: { ...response, isTemporary: payload.isTemporary },
|
||||
});
|
||||
.subscribe((response: Playlist) => {
|
||||
this.store.dispatch(
|
||||
PlaylistActions.handleAddingPlaylistByUrl({
|
||||
isTemporary: !!payload?.isTemporary,
|
||||
playlist: response,
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -196,11 +205,15 @@ export class PwaService extends DataService {
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
if (payload.params.action === 'get_account_info') return;
|
||||
window.postMessage({
|
||||
type: ERROR,
|
||||
status: error.error?.status,
|
||||
message: error.error?.message ?? 'Unknown error',
|
||||
});
|
||||
this.snackBar.open(
|
||||
`Error: ${error.message ?? ' Unknown error'}, status: ${
|
||||
error.status ?? 500
|
||||
}`,
|
||||
'Close',
|
||||
{
|
||||
duration: 5000,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,11 +245,14 @@ export class PwaService extends DataService {
|
||||
return (await response.json()).payload;
|
||||
} catch (err) {
|
||||
console.error('Stalker request error:', err);
|
||||
window.postMessage({
|
||||
type: ERROR,
|
||||
message: err.message ?? 'Error: not found',
|
||||
status: err.status ?? 404,
|
||||
});
|
||||
|
||||
this.snackBar.open(
|
||||
`Error: ${err.message ?? ' Not found'}, status: ${err.status ?? 404}`,
|
||||
'Close',
|
||||
{
|
||||
duration: 5000,
|
||||
}
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
|
||||
&.selected {
|
||||
background-color: rgba(63, 81, 181, 0.1);
|
||||
border-left: 4px solid #3f51b5;
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
+8
-7
@@ -1,20 +1,21 @@
|
||||
<div class="expired-account">
|
||||
@if (viewType === 'ERROR') {
|
||||
<img src="./assets/images/broken-tv.svg" />
|
||||
} @else if (viewType === 'EMPTY_CATEGORY') {
|
||||
@if (viewType() === 'ERROR') {
|
||||
<img alt="Something went wrong" src="./assets/images/broken-tv.svg" />
|
||||
} @else if (viewType() === 'EMPTY_CATEGORY') {
|
||||
<img
|
||||
alt="empty category"
|
||||
class="empty-category-img"
|
||||
src="./assets/images/empty-category.svg"
|
||||
/>
|
||||
} @else if (viewType === 'NO_SEARCH_RESULTS') {
|
||||
} @else if (viewType() === 'NO_SEARCH_RESULTS') {
|
||||
<mat-icon class="icon">search</mat-icon>
|
||||
}
|
||||
<div>
|
||||
<h3>{{ title }}</h3>
|
||||
{{ description }}
|
||||
<h3>{{ title() }}</h3>
|
||||
{{ description() }}
|
||||
</div>
|
||||
|
||||
@if (showActionButtons) {
|
||||
@if (showActionButtons()) {
|
||||
<div>
|
||||
<button mat-stroked-button routerLink="/">
|
||||
<mat-icon>home</mat-icon>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, Input, inject } from '@angular/core';
|
||||
import { Component, inject, input } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
@@ -25,13 +25,16 @@ export class PlaylistErrorViewComponent {
|
||||
private store = inject(Store);
|
||||
private translate = inject(TranslateService);
|
||||
|
||||
currentPlaylist = this.store.selectSignal(selectCurrentPlaylist);
|
||||
private readonly currentPlaylist = this.store.selectSignal(
|
||||
selectCurrentPlaylist
|
||||
);
|
||||
|
||||
@Input() description: string;
|
||||
@Input() showActionButtons = true;
|
||||
@Input() title: string;
|
||||
@Input() viewType: 'ERROR' | 'EMPTY_CATEGORY' | 'NO_SEARCH_RESULTS' =
|
||||
'ERROR';
|
||||
readonly description = input<string>(undefined);
|
||||
readonly showActionButtons = input(true);
|
||||
readonly title = input<string>(undefined);
|
||||
readonly viewType = input<'ERROR' | 'EMPTY_CATEGORY' | 'NO_SEARCH_RESULTS'>(
|
||||
'ERROR'
|
||||
);
|
||||
|
||||
openPlaylistDetails() {
|
||||
this.dialog.open(PlaylistInfoComponent, {
|
||||
|
||||
+12
-12
@@ -7,7 +7,7 @@ mat-nav-list {
|
||||
}
|
||||
|
||||
mat-list-item {
|
||||
width: 280px !important;
|
||||
width: 370px !important;
|
||||
}
|
||||
|
||||
.scroll-viewport-portals {
|
||||
@@ -33,7 +33,7 @@ mat-list-item {
|
||||
transition: background-color 0.2s ease;
|
||||
border-radius: 4px;
|
||||
margin: 2px 8px;
|
||||
|
||||
|
||||
::ng-deep .mdc-list-item__content {
|
||||
overflow: hidden;
|
||||
flex: 1;
|
||||
@@ -50,7 +50,7 @@ mat-list-item {
|
||||
}
|
||||
|
||||
&.mat-mdc-list-item-active {
|
||||
background-color: rgba(29, 185, 84, 0.2);
|
||||
background-color: rgba(29, 185, 84, 0.2);
|
||||
.channel-info .title {
|
||||
color: #1db954;
|
||||
}
|
||||
@@ -73,7 +73,7 @@ mat-list-item {
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
div[matformfieldlineripple] {
|
||||
display: none;
|
||||
}
|
||||
@@ -93,7 +93,7 @@ mat-list-item {
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 4px;
|
||||
|
||||
|
||||
.title {
|
||||
font-weight: 600; // Slightly bolder
|
||||
font-size: 14px;
|
||||
@@ -108,7 +108,7 @@ mat-list-item {
|
||||
animation: marquee 6s linear infinite;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.current-program {
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
@@ -116,7 +116,7 @@ mat-list-item {
|
||||
text-overflow: ellipsis;
|
||||
line-height: 1.2;
|
||||
margin-top: 2px;
|
||||
|
||||
|
||||
&:hover {
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
@@ -135,17 +135,17 @@ mat-list-item {
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 11px;
|
||||
opacity: .6;
|
||||
opacity: 0.6;
|
||||
padding: 0 2px;
|
||||
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 3px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
opacity: .6;
|
||||
opacity: 0.6;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
|
||||
.progress {
|
||||
height: 100%;
|
||||
background: #1db954;
|
||||
@@ -166,7 +166,7 @@ mat-list-item {
|
||||
|
||||
.progress-container {
|
||||
opacity: 0.5;
|
||||
|
||||
|
||||
.progress-bar {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
.actions {
|
||||
button {
|
||||
margin: 5px 0;
|
||||
margin: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,9 @@ import * as PlaylistActions from 'm3u-state';
|
||||
import { selectCurrentPlaylist } from 'm3u-state';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
|
||||
/**
|
||||
* @deprecated: there is another one in xtream-tauri
|
||||
*/
|
||||
@Component({
|
||||
selector: 'app-playlist-error-view',
|
||||
templateUrl: './playlist-error-view.component.html',
|
||||
|
||||
@@ -66,7 +66,8 @@
|
||||
"MY_PLAYLISTS": "My playlists",
|
||||
"MY_PLAYLISTS_SUBTITLE": "all available playlists",
|
||||
"PLAYLIST_UPDATE_SUCCESS": "Success! The playlist was successfully updated.",
|
||||
"PLAYLIST_UPDATE_ERROR": "Error updating playlist details."
|
||||
"PLAYLIST_UPDATE_ERROR": "Error updating playlist details.",
|
||||
"AUTO_REFRESH_UPDATE_SUCCESS": "Success! The playlists were successfully updated"
|
||||
},
|
||||
"FILE_UPLOAD": {
|
||||
"DRAG_DROP": "Drag & drop files here",
|
||||
|
||||
@@ -133,3 +133,8 @@ export const setCurrentPlaylistId = createAction(
|
||||
`${STORE_KEY} Set current playlist id`,
|
||||
props<{ playlistId: string | undefined }>()
|
||||
);
|
||||
|
||||
export const handleAddingPlaylistByUrl = createAction(
|
||||
`${PLAYLISTS_STORE_KEY} Handle adding playlist by URL`,
|
||||
props<{ isTemporary: boolean; playlist: Playlist }>()
|
||||
);
|
||||
@@ -7,6 +7,7 @@ import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
combineLatestWith,
|
||||
EMPTY,
|
||||
firstValueFrom,
|
||||
map,
|
||||
switchMap,
|
||||
@@ -239,10 +240,16 @@ export class PlaylistEffects {
|
||||
addPlaylist$ = createEffect(
|
||||
() => {
|
||||
return this.actions$.pipe(
|
||||
ofType(PlaylistActions.addPlaylist),
|
||||
switchMap((action) =>
|
||||
this.playlistsService.addPlaylist(action.playlist)
|
||||
ofType(
|
||||
PlaylistActions.addPlaylist,
|
||||
PlaylistActions.handleAddingPlaylistByUrl
|
||||
),
|
||||
switchMap((action) => {
|
||||
if ('isTemporary' in action && action.isTemporary) {
|
||||
return EMPTY;
|
||||
}
|
||||
return this.playlistsService.addPlaylist(action.playlist);
|
||||
}),
|
||||
map((playlist: Playlist) => {
|
||||
if (playlist.serverUrl && !this.dataService.isElectron) {
|
||||
this.router.navigate(['/xtreams/', playlist._id]);
|
||||
|
||||
@@ -254,6 +254,25 @@ export const playlistReducer = createReducer(
|
||||
currentPlaylistId: playlistId,
|
||||
};
|
||||
}
|
||||
),
|
||||
on(
|
||||
PlaylistActions.handleAddingPlaylistByUrl,
|
||||
(state, action): PlaylistState => {
|
||||
if (action.isTemporary) {
|
||||
return {
|
||||
...state,
|
||||
channels: action.playlist.playlist.items as Channel[],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.addOne(
|
||||
action.playlist,
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import { XTREAM_REQUEST } from 'shared-interfaces';
|
||||
import { DataService } from './data.service';
|
||||
|
||||
export type PortalStatus = 'active' | 'inactive' | 'expired' | 'unavailable';
|
||||
@@ -29,14 +28,17 @@ export class PortalStatusService {
|
||||
normalizedUrl = serverUrl;
|
||||
}
|
||||
|
||||
let response = await this.dataService.sendIpcEvent(XTREAM_REQUEST, {
|
||||
url: normalizedUrl,
|
||||
params: {
|
||||
password,
|
||||
username,
|
||||
action: 'get_account_info',
|
||||
},
|
||||
});
|
||||
let response = await this.dataService.sendIpcEvent(
|
||||
'XTREAM_REQUEST',
|
||||
{
|
||||
url: normalizedUrl,
|
||||
params: {
|
||||
password,
|
||||
username,
|
||||
action: 'get_account_info',
|
||||
},
|
||||
}
|
||||
);
|
||||
response = response?.payload;
|
||||
|
||||
if (!response?.user_info?.status) {
|
||||
|
||||
@@ -11,9 +11,7 @@ export const EPG_FORCE_FETCH = 'EPG:EPG_FORCE_FETCH';
|
||||
|
||||
// Playlist related commands
|
||||
export const PLAYLIST_PARSE_BY_URL = 'PLAYLIST:PARSE_PLAYLIST_BY_URL';
|
||||
export const PLAYLIST_PARSE_RESPONSE = 'PLAYLIST:PARSE_PLAYLIST_RESPONSE';
|
||||
export const PLAYLIST_UPDATE = 'PLAYLIST:UPDATE';
|
||||
export const PLAYLIST_UPDATE_RESPONSE = 'PLAYLIST:UPDATE_RESPONSE';
|
||||
|
||||
// General
|
||||
export const ERROR = 'ERROR';
|
||||
|
||||
Reference in new issue
Block a user