mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 01:56:16 -08:00
Merge pull request #450 from KiPSOFT/remote-control-on-http
Simple remote control support on http
This commit is contained in:
20 files changed
+304
-8
No files matched your search
@@ -22,6 +22,7 @@ preload.js
|
||||
shared/*.js
|
||||
playwright-report
|
||||
.nx
|
||||
server.js
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
+13
-1
@@ -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 {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
+1
-1
@@ -120,7 +120,7 @@ try {
|
||||
const menu = new AppMenu(win);
|
||||
Menu.setApplicationMenu(menu.getMenu());
|
||||
api.setMainWindow(win);
|
||||
|
||||
|
||||
// create hidden window for epg worker
|
||||
createEpgWorkerWindow();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>{{it.title}}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
header {
|
||||
width: 100%;
|
||||
background-color: #f8f9fa;
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
}
|
||||
header h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
color: white;
|
||||
}
|
||||
.up-channel {
|
||||
background-color: #007bff;
|
||||
}
|
||||
.down-channel {
|
||||
background-color: #28a745;
|
||||
}
|
||||
footer {
|
||||
margin-top: 30px;
|
||||
text-align: center;
|
||||
}
|
||||
footer p {
|
||||
font-size: 14px;
|
||||
color: #6c757d;
|
||||
}
|
||||
</style>
|
||||
<script>
|
||||
// AJAX isteği yapmak için fetch API kullanımı
|
||||
function callAjax(url) {
|
||||
fetch(url)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Network response was not ok');
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
console.log('Success:', data);
|
||||
alert('AJAX request successful! Check console for data.');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('There was a problem with your fetch operation:', error);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>{{it.headerText}}</h1>
|
||||
</header>
|
||||
|
||||
<div class="button-container">
|
||||
<button class="up-channel" onclick="callAjax('{{it.upChannelUrl}}')">{{it.upChannelText}}</button>
|
||||
<button class="down-channel" onclick="callAjax('{{it.downChannelUrl}}')">{{it.downChannelText}}</button>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<p>{{it.footer}}</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,86 @@
|
||||
import { IncomingMessage, ServerResponse } from "http";
|
||||
import { REMOTE_CONTROL_CHANGE_CHANNEL } from "../shared/ipc-commands";
|
||||
|
||||
const http = require('http');
|
||||
const Sqrl = require('squirrelly');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
export class Server {
|
||||
/** Server instance */
|
||||
server: any;
|
||||
/** Api instance */
|
||||
api: any;
|
||||
|
||||
/**
|
||||
* Creates a new server instance
|
||||
* @param port port number
|
||||
* @param api application instance
|
||||
*/
|
||||
constructor(_api: any) {
|
||||
this.api = _api;
|
||||
this.server = http.createServer(this.requestListener.bind(this));
|
||||
}
|
||||
|
||||
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];
|
||||
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: 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: 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.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');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+13
@@ -44,6 +44,7 @@
|
||||
"node-mpv": "github:4gray/Node-MPV",
|
||||
"rxjs": "7.8.1",
|
||||
"semver": "7.5.2",
|
||||
"squirrelly": "9.1.0",
|
||||
"uuid": "9.0.0",
|
||||
"video.js": "7.20.3",
|
||||
"videojs-contrib-quality-levels": "2.2.0",
|
||||
@@ -30680,6 +30681,18 @@
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/squirrelly": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/squirrelly/-/squirrelly-9.1.0.tgz",
|
||||
"integrity": "sha512-kvjFqb7qzC4gX4lkqSaU8QPvUHhDLMiDpxpz7a66vjTH0JtjLJqAXbPrc7ST61EefuuuW05sne2rjGskunrF2A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/squirrellyjs/squirrelly?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ssri": {
|
||||
"version": "10.0.6",
|
||||
"resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz",
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
"node-mpv": "github:4gray/Node-MPV",
|
||||
"rxjs": "7.8.1",
|
||||
"semver": "7.5.2",
|
||||
"squirrelly": "9.1.0",
|
||||
"uuid": "9.0.0",
|
||||
"video.js": "7.20.3",
|
||||
"videojs-contrib-quality-levels": "2.2.0",
|
||||
|
||||
@@ -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';
|
||||
@@ -219,7 +219,7 @@ describe('AppComponent', () => {
|
||||
|
||||
expect(spyOnSettingsGet).toHaveBeenCalledWith(STORE_KEY.Settings);
|
||||
expect(settingsService.changeTheme).toHaveBeenCalledWith(theme);
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledTimes(1);
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledTimes(2);
|
||||
expect(translateService.use).toHaveBeenCalledWith(language);
|
||||
});
|
||||
|
||||
@@ -245,7 +245,7 @@ describe('AppComponent', () => {
|
||||
|
||||
expect(spyOnSettingsGet).toHaveBeenCalledWith(STORE_KEY.Settings);
|
||||
expect(settingsService.changeTheme).toHaveBeenCalledWith(theme);
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledTimes(0);
|
||||
expect(electronService.sendIpcEvent).toHaveBeenCalledTimes(1);
|
||||
expect(translateService.use).toHaveBeenCalledWith(defaultLanguage);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
trackBy: trackBy;
|
||||
templateCacheSize: 0
|
||||
"
|
||||
[selected]="item.stream_id === activeLiveStream?.stream_id"
|
||||
[value]="item"
|
||||
(click)="itemClicked.emit(item)"
|
||||
>
|
||||
|
||||
@@ -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<XtreamItem>();
|
||||
|
||||
|
||||
@@ -200,6 +200,36 @@
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.REMOTE_CONTROL' | translate }}
|
||||
<p>{{ 'SETTINGS.REMOTE_CONTROL_DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
<div class="column" style="margin-right: 10px">
|
||||
<mat-checkbox
|
||||
class="column"
|
||||
formControlName="remoteControl"
|
||||
></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div class="row" *ngIf="settingsForm.value.remoteControl === true">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.REMOTE_CONTROL_PORT' | translate }}
|
||||
<p>{{ 'SETTINGS.REMOTE_CONTROL_PORT_DESCRIPTION' | translate }}</p>
|
||||
</div>
|
||||
<div class="column" style="margin-right: 10px">
|
||||
<mat-form-field appearance="outline" class="full-width">
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
id="remoteControlPort"
|
||||
formControlName="remoteControlPort"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
<mat-divider></mat-divider>
|
||||
<div class="row">
|
||||
<div class="column">
|
||||
{{ 'SETTINGS.VERSION' | translate }}
|
||||
|
||||
@@ -62,6 +62,8 @@ const DEFAULT_SETTINGS = {
|
||||
theme: Theme.LightTheme,
|
||||
mpvPlayerPath: '',
|
||||
vlcPlayerPath: '',
|
||||
remoteControl: false,
|
||||
remoteControlPort: 3000
|
||||
};
|
||||
|
||||
describe('SettingsComponent', () => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,4 +23,6 @@ export interface Settings {
|
||||
theme: Theme;
|
||||
mpvPlayerPath: string;
|
||||
vlcPlayerPath: string;
|
||||
remoteControl: boolean;
|
||||
remoteControlPort: number;
|
||||
}
|
||||
@@ -36,6 +36,7 @@
|
||||
[player]="player"
|
||||
[streamUrl]="streamUrl"
|
||||
[epgItems]="epgItems"
|
||||
[activeLiveStream]="activeLiveStream"
|
||||
(itemClicked)="itemClicked($event)"
|
||||
/>
|
||||
} @else {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-2
@@ -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",
|
||||
@@ -283,5 +287,12 @@
|
||||
"STREAM_URL_COPIED": "Stream URL copied to clipboard",
|
||||
"COPY_STREAM_URL": "Copy Stream URL"
|
||||
},
|
||||
"INFORMATION": "Information"
|
||||
"INFORMATION": "Information",
|
||||
"REMOTE_CONTROL": {
|
||||
"TITLE": "Remote control",
|
||||
"UP_CHANNEL": "Up channel",
|
||||
"DOWN_CHANNEL": "Down channel",
|
||||
"HEADER": "IPTVNator Remote Control",
|
||||
"FOOTER": "IPTVNator Remote Control"
|
||||
}
|
||||
}
|
||||
+13
-2
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user