diff --git a/electron/api.ts b/electron/api.ts index c523b348c..f12b1373e 100644 --- a/electron/api.ts +++ b/electron/api.ts @@ -32,6 +32,7 @@ import { PLAYLIST_UPDATE_RESPONSE, SET_MPV_PLAYER_PATH, SET_VLC_PLAYER_PATH, + SETTINGS_UPDATE, STALKER_REQUEST, STALKER_RESPONSE, XTREAM_REQUEST, @@ -40,6 +41,7 @@ import { import { Playlist } from '../shared/playlist.interface'; import { createPlaylistObject } from '../shared/playlist.utils'; import { ParsedPlaylist } from '../src/typings.d'; +import { Server } from './server'; const fs = require('fs'); const https = require('https'); @@ -88,7 +90,12 @@ export class Api { mpv; + settings; + + server; + constructor(store) { + this.server = new Server(this); this.store = store; this.mpv = this.createMpvInstance(); this.mpv @@ -286,7 +293,11 @@ export class Api { .on(SET_VLC_PLAYER_PATH, (_event, vlcPlayerPath) => { console.log('... setting vlc player path', vlcPlayerPath); store.set(VLC_PLAYER_PATH, vlcPlayerPath); - }); + }) + .on(SETTINGS_UPDATE, (_event, arg) => { + this.settings = arg; + this.server.updateSettings(); + }) ; // listeners for EPG events ipcMain @@ -625,4 +636,5 @@ export class Api { } }); } + } diff --git a/electron/main.ts b/electron/main.ts index 07c5c1aa4..4900d05ba 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -4,7 +4,6 @@ import * as path from 'path'; import * as url from 'url'; import { Api } from './api'; import { AppMenu } from './menu'; -import { Server } from './server'; const fixPath = require('fix-path'); const { @@ -121,8 +120,7 @@ try { const menu = new AppMenu(win); Menu.setApplicationMenu(menu.getMenu()); api.setMainWindow(win); - new Server(3000, api); - + // create hidden window for epg worker createEpgWorkerWindow(); }); diff --git a/electron/server.ts b/electron/server.ts index 2c55d787f..bc603e700 100644 --- a/electron/server.ts +++ b/electron/server.ts @@ -1,4 +1,5 @@ import { IncomingMessage, ServerResponse } from "http"; +import { REMOTE_CONTROL_CHANGE_CHANNEL } from "../shared/ipc-commands"; const http = require('http'); const Sqrl = require('squirrelly'); @@ -8,47 +9,74 @@ const path = require('path'); export class Server { /** Server instance */ server: any; - /** Port number */ - port: number; /** Api instance */ api: any; - + /** * Creates a new server instance * @param port port number * @param api application instance */ - constructor(_port: number, _api: any) { + constructor(_api: any) { this.api = _api; - this.port = _port; this.server = http.createServer(this.requestListener.bind(this)); - this.server.listen(this.port); + } + + updateSettings() { + if (this.api.settings.remoteControl && !this.server.listening) { + this.server.listen(this.api.settings.remoteControlPort); + } else if (this.server.listening && !this.api.settings.remoteControl) { + this.server.close(); + } + } + + async getTranslation() { + const language = this.api.settings && this.api.settings.language ? this.api.settings.language : 'en'; + const languageFileContent = await fs.promises.readFile(path.join(__dirname, `../src/assets/i18n/${language}.json`), 'utf-8'); + return JSON.parse(languageFileContent); + } + + getTranslationValue(key: string, translation: any, defaultValue: string) { + if (translation && translation.REMOTE_CONTROL) { + const translationValue = translation.REMOTE_CONTROL[key]; + if (translationValue) { + return translationValue; + } + } + return defaultValue; } async requestListener(request: IncomingMessage, response: ServerResponse) { const currentPage = request.url?.split('?')[0]?.split('/').splice(-1)[0]; - console.log(this.api.store); + const translation = await this.getTranslation(); switch (currentPage) { case '': { const indexFile = await fs.promises.readFile(path.join(__dirname, './remote-control/', 'index.sqrl'), 'utf-8'); const renderedHtml = Sqrl.render(indexFile, { - headerText: 'My Dynamic Header', - upChannelText: 'Up Channel', - downChannelText: 'Down Channel', + headerText: this.getTranslationValue('HEADER', translation, 'Remote Control'), + upChannelText: this.getTranslationValue('UP_CHANNEL', translation, 'Up Channel'), + downChannelText: this.getTranslationValue('DOWN_CHANNEL', translation, 'Down Channel'), upChannelUrl: '/upChannel', downChannelUrl: '/downChannel', - title: 'IPTVNator' + title: this.getTranslationValue('TITLE', translation, 'IPTVNator'), + footer: this.getTranslationValue('FOOTER', translation, 'IPTVNator') }); response.writeHead(200, { 'Content-Type': 'text/html' }); response.end(renderedHtml); break; } case 'upChannel': { - // this.api.upChannel(); + this.api.mainWindow.webContents.send(REMOTE_CONTROL_CHANGE_CHANNEL, { type: 'up' }); response.writeHead(200, { 'Content-Type': 'text/html' }); response.end('Up Channel'); break; } + case 'downChannel': { + this.api.mainWindow.webContents.send(REMOTE_CONTROL_CHANGE_CHANNEL, { type: 'down' }); + response.writeHead(200, { 'Content-Type': 'text/html' }); + response.end('Down Channel'); + break; + } default: response.writeHead(404, { 'Content-Type': 'text/html' }); response.end('404'); diff --git a/shared/ipc-commands.ts b/shared/ipc-commands.ts index a8ab6408c..e7057b4ab 100644 --- a/shared/ipc-commands.ts +++ b/shared/ipc-commands.ts @@ -54,3 +54,9 @@ export const XTREAM_RESPONSE = 'XTREAM_RESPONSE'; // Stalker export const STALKER_REQUEST = 'STALKER_REQUEST'; export const STALKER_RESPONSE = 'STALKER_RESPONSE'; + +// Settings +export const SETTINGS_UPDATE = 'SETTINGS_UPDATE'; + +// Remote Control +export const REMOTE_CONTROL_CHANGE_CHANNEL = 'REMOTE_CONTROL_CHANGE_CHANNEL'; \ No newline at end of file diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 622d6b4d2..08244bd00 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -13,6 +13,7 @@ import { EPG_FETCH_DONE, ERROR, OPEN_FILE, + SETTINGS_UPDATE, SHOW_WHATS_NEW, VIEW_ADD_PLAYLIST, VIEW_SETTINGS, @@ -148,6 +149,7 @@ export class AppComponent { .getValueFromLocalStorage(STORE_KEY.Settings) .subscribe((settings: Settings) => { if (settings && Object.keys(settings).length > 0) { + this.electronService.sendIpcEvent(SETTINGS_UPDATE, settings); this.translate.use(settings.language ?? this.DEFAULT_LANG); if ( settings.epgUrl?.length > 0 && diff --git a/src/app/portals/live-stream-layout/live-stream-layout.component.html b/src/app/portals/live-stream-layout/live-stream-layout.component.html index 310b84152..3090dbde8 100644 --- a/src/app/portals/live-stream-layout/live-stream-layout.component.html +++ b/src/app/portals/live-stream-layout/live-stream-layout.component.html @@ -30,6 +30,7 @@ trackBy: trackBy; templateCacheSize: 0 " + [selected]="item.stream_id === activeLiveStream?.stream_id" [value]="item" (click)="itemClicked.emit(item)" > diff --git a/src/app/portals/live-stream-layout/live-stream-layout.component.ts b/src/app/portals/live-stream-layout/live-stream-layout.component.ts index d92d82ca4..74f7d52f3 100644 --- a/src/app/portals/live-stream-layout/live-stream-layout.component.ts +++ b/src/app/portals/live-stream-layout/live-stream-layout.component.ts @@ -46,6 +46,7 @@ export class LiveStreamLayoutComponent { @Input({ required: true }) player: VideoPlayer = VideoPlayer.VideoJs; @Input() epgItems: EpgItem[]; @Input() streamUrl: string; + @Input() activeLiveStream: XtreamItem; @Output() itemClicked = new EventEmitter(); diff --git a/src/app/settings/settings.component.html b/src/app/settings/settings.component.html index f724c58f9..73dfe885d 100644 --- a/src/app/settings/settings.component.html +++ b/src/app/settings/settings.component.html @@ -200,6 +200,36 @@ +
+
+ {{ 'SETTINGS.REMOTE_CONTROL' | translate }} +

{{ 'SETTINGS.REMOTE_CONTROL_DESCRIPTION' | translate }}

+
+
+ +
+
+ +
+
+ {{ 'SETTINGS.REMOTE_CONTROL_PORT' | translate }} +

{{ 'SETTINGS.REMOTE_CONTROL_PORT_DESCRIPTION' | translate }}

+
+
+ + + +
+
+
{{ 'SETTINGS.VERSION' | translate }} diff --git a/src/app/settings/settings.component.ts b/src/app/settings/settings.component.ts index e48d72de1..7f99f6f7d 100644 --- a/src/app/settings/settings.component.ts +++ b/src/app/settings/settings.component.ts @@ -25,6 +25,7 @@ import { EPG_FORCE_FETCH, SET_MPV_PLAYER_PATH, SET_VLC_PLAYER_PATH, + SETTINGS_UPDATE, } from '../../../shared/ipc-commands'; import { Playlist } from '../../../shared/playlist.interface'; import { DataService } from '../services/data.service'; @@ -111,6 +112,8 @@ export class SettingsComponent implements OnInit { theme: Theme.LightTheme, mpvPlayerPath: '', vlcPlayerPath: '', + remoteControl: false, + remoteControlPort: 3000 }); /** Form array with epg sources */ @@ -161,6 +164,8 @@ export class SettingsComponent implements OnInit { theme: settings.theme ?? Theme.LightTheme, mpvPlayerPath: settings.mpvPlayerPath ?? '', vlcPlayerPath: settings.vlcPlayerPath ?? '', + remoteControl: settings.remoteControl ?? false, + remoteControlPort: settings.remoteControlPort ?? 3000 }); } catch (error) { throw new Error(error); @@ -250,6 +255,8 @@ export class SettingsComponent implements OnInit { this.applyChangedSettings(); }); + this.electronService.sendIpcEvent(SETTINGS_UPDATE, this.settingsForm.value); + this.electronService.sendIpcEvent( SET_MPV_PLAYER_PATH, this.settingsForm.value.mpvPlayerPath diff --git a/src/app/settings/settings.interface.ts b/src/app/settings/settings.interface.ts index f31a3f981..e521ae7c6 100644 --- a/src/app/settings/settings.interface.ts +++ b/src/app/settings/settings.interface.ts @@ -23,4 +23,6 @@ export interface Settings { theme: Theme; mpvPlayerPath: string; vlcPlayerPath: string; + remoteControl: boolean; + remoteControlPort: number; } diff --git a/src/app/xtream/xtream-main-container.component.html b/src/app/xtream/xtream-main-container.component.html index e32d28725..0c581c843 100644 --- a/src/app/xtream/xtream-main-container.component.html +++ b/src/app/xtream/xtream-main-container.component.html @@ -36,6 +36,7 @@ [player]="player" [streamUrl]="streamUrl" [epgItems]="epgItems" + [activeLiveStream]="activeLiveStream" (itemClicked)="itemClicked($event)" /> } @else { diff --git a/src/app/xtream/xtream-main-container.component.ts b/src/app/xtream/xtream-main-container.component.ts index 5d4869e77..34d48b55f 100644 --- a/src/app/xtream/xtream-main-container.component.ts +++ b/src/app/xtream/xtream-main-container.component.ts @@ -21,6 +21,7 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import { Store } from '@ngrx/store'; import { IpcCommand } from '../../../shared/ipc-command.class'; import { + REMOTE_CONTROL_CHANGE_CHANNEL, ERROR, OPEN_MPV_PLAYER, OPEN_VLC_PLAYER, @@ -183,6 +184,7 @@ export class XtreamMainContainerComponent implements OnInit { streamUrl: string; epgItems = []; hideExternalInfoDialog = this.portalStore.hideExternalInfoDialog; + activeLiveStream: XtreamLiveStream; commandsList = [ new IpcCommand(XTREAM_RESPONSE, (response: XtreamResponse) => @@ -191,6 +193,9 @@ export class XtreamMainContainerComponent implements OnInit { new IpcCommand(ERROR, (response: { message: string; status: number }) => this.showErrorAsNotification(response) ), + new IpcCommand(REMOTE_CONTROL_CHANGE_CHANNEL, (response: { type: 'up' | 'down' }) => { + this.remoteControlChangeChannel(response.type); + }) ]; constructor() { @@ -345,6 +350,7 @@ export class XtreamMainContainerComponent implements OnInit { playLiveStream(item: XtreamLiveStream) { const { serverUrl, username, password } = this.currentPlaylist(); const streamUrl = `${serverUrl}/${item.stream_type}/${username}/${password}/${item.stream_id}.m3u8`; + this.activeLiveStream = item; this.openPlayer(streamUrl, item.name); } @@ -530,4 +536,22 @@ export class XtreamMainContainerComponent implements OnInit { this.currentLayout = 'favorites'; this.setInitialBreadcrumb(); } + + remoteControlChangeChannel(type: 'up' | 'down') { + if (this.currentLayout === 'category_content' && this.activeLiveStream) { + let nextItem; + const index = this.activeLiveStream.num - 1; + if (type === 'up') { + if (index - 1 >= 0) { + nextItem = this.items[index - 1]; + } + } else if (type === 'down') { + const index = this.activeLiveStream.num - 1; + if (index + 1 < this.items.length) { + nextItem = this.items[index + 1]; + } + } + this.itemClicked(nextItem); + } + } } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index 50690f96b..3a03add75 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -138,7 +138,11 @@ "MPV_PLAYER_PATH": "mpv player path", "VLC_PLAYER_PATH_LABEL": "VLC path", "VLC_PLAYER_PATH_DESCRIPTION": "Set path to VLC player in your system. It will use the provided path to a VLC binary instead of the default path for our system.", - "VLC_PLAYER_PATH": "VLC player path" + "VLC_PLAYER_PATH": "VLC player path", + "REMOTE_CONTROL": "Remote control", + "REMOTE_CONTROL_DESCRIPTION": "Enable remote control of the application.", + "REMOTE_CONTROL_PORT": "Remote control port", + "REMOTE_CONTROL_PORT_DESCRIPTION": "Port used for remote control of the application. Port 3000 is used by default." }, "THEMES": { "DARK_THEME": "Dark theme", diff --git a/src/assets/i18n/tr.json b/src/assets/i18n/tr.json index 51d0e4e36..54cca15d4 100644 --- a/src/assets/i18n/tr.json +++ b/src/assets/i18n/tr.json @@ -138,7 +138,11 @@ "MPV_PLAYER_PATH": "mpv oynatıcı yolu", "VLC_PLAYER_PATH_LABEL": "VLC yolu", "VLC_PLAYER_PATH_DESCRIPTION": "Sisteminizdeki VLC oynatıcı yolunu ayarlayın. Sistemimiz için varsayılan yolu kullanmak yerine, sağlanan VLC ikili dosyası yolunu kullanacaktır.", - "VLC_PLAYER_PATH": "VLC oynatıcı yolu" + "VLC_PLAYER_PATH": "VLC oynatıcı yolu", + "REMOTE_CONTROL": "Uzaktan kontrol", + "REMOTE_CONTROL_DESCRIPTION": "Uygulamanın uzaktan kontrolünü etkinleştirin.", + "REMOTE_CONTROL_PORT": "Uzaktan kontrol portu", + "REMOTE_CONTROL_PORT_DESCRIPTION": "Uzaktan kontrol için bir port belirleyin. Varsayılan olarak 3080 kullanılır." }, "THEMES": { "DARK_THEME": "Koyu Tema", @@ -283,5 +287,12 @@ "STREAM_URL_COPIED": "Yayın URL'si panoya kopyalandı", "COPY_STREAM_URL": "Yayın URL'sini Kopyala" }, - "INFORMATION": "Bilgi" + "INFORMATION": "Bilgi", + "REMOTE_CONTROL": { + "TITLE": "Uzaktan Kontrol", + "UP_CHANNEL": "Yukarı Kanal", + "DOWN_CHANNEL": "Aşağı Kanal", + "HEADER": "IPTVNator Uzaktan Kontrol", + "FOOTER": "IPTVNator Uzaktan Kontrol" + } }