Merge pull request #652 from 4gray/feat-remote-control-ui-server-integration

feat-remote-control-ui-server-integration
This commit is contained in:
4gray authored and GitHub committed 2025-11-22 11:36:02 +01:00
commit 5796dd0a48
49 files changed
+2407 -790

No files matched your search

+2 -1
View File
@@ -7,7 +7,7 @@
"build": {
"dependsOn": [
{
"projects": ["web"],
"projects": ["web", "remote-control-web"],
"target": "build"
}
],
@@ -54,6 +54,7 @@
"nx run electron-backend:serve-electron"
]
}
},
"serve-electron": {
"executor": "nx-electron:execute",
@@ -1,6 +1,10 @@
import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electron', {
// Remote control channel change listener
onChannelChange: (callback: (data: { direction: 'up' | 'down' }) => void) => {
ipcRenderer.on('CHANNEL_CHANGE', (_event, data) => callback(data));
},
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
platform: process.platform,
fetchPlaylistByUrl: (url: string, title?: string) =>
@@ -0,0 +1,107 @@
import { BrowserWindow, ipcMain } from 'electron';
import * as http from 'http';
import { httpServer } from '../server/http-server';
import { store } from '../services/store.service';
class RemoteControlEvents {
/**
* Bootstrap remote control events
*/
bootstrapRemoteControlEvents(): void {
// Register HTTP API endpoints
httpServer.registerRemoteControlHandler(
'/api/remote-control/channel/up',
this.handleChannelUp.bind(this)
);
httpServer.registerRemoteControlHandler(
'/api/remote-control/channel/down',
this.handleChannelDown.bind(this)
);
// Start HTTP server if remote control is enabled in settings
const remoteControlEnabled = store.get('remoteControl', false);
const remoteControlPort = store.get('remoteControlPort', 8765);
if (remoteControlEnabled) {
httpServer.start(remoteControlPort);
}
// Register IPC handlers (for when the main app wants to use remote control)
ipcMain.handle('REMOTE_CONTROL_CHANNEL_UP', () => {
return this.changeChannelUp();
});
ipcMain.handle('REMOTE_CONTROL_CHANNEL_DOWN', () => {
return this.changeChannelDown();
});
}
/**
* HTTP handler for channel up
*/
private handleChannelUp(
req: http.IncomingMessage,
res: http.ServerResponse
): void {
if (req.method !== 'POST') {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method not allowed' }));
return;
}
this.changeChannelUp();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true }));
}
/**
* HTTP handler for channel down
*/
private handleChannelDown(
req: http.IncomingMessage,
res: http.ServerResponse
): void {
if (req.method !== 'POST') {
res.writeHead(405, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Method not allowed' }));
return;
}
this.changeChannelDown();
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true }));
}
/**
* Change to the next channel
*/
private changeChannelUp(): void {
console.log('Channel UP requested');
this.sendChannelChangeToRenderer('up');
}
/**
* Change to the previous channel
*/
private changeChannelDown(): void {
console.log('Channel DOWN requested');
this.sendChannelChangeToRenderer('down');
}
/**
* Send channel change message to the renderer process
*/
private sendChannelChangeToRenderer(direction: 'up' | 'down'): void {
const windows = BrowserWindow.getAllWindows();
if (windows.length > 0) {
windows[0].webContents.send('CHANNEL_CHANGE', { direction });
console.log(`Sent CHANNEL_CHANGE ${direction} to renderer`);
} else {
console.warn('No browser windows found to send channel change');
}
}
}
export default new RemoteControlEvents();
@@ -3,6 +3,7 @@ import {
MPV_REUSE_INSTANCE,
store,
} from '../services/store.service';
import { httpServer } from '../server/http-server';
export default class SettingsEvents {
static bootstrapSettingsEvents(): Electron.IpcMain {
@@ -12,9 +13,26 @@ export default class SettingsEvents {
ipcMain.handle('SETTINGS_UPDATE', (_event, arg) => {
console.log('Received SETTINGS_UPDATE with data:', arg);
// Only set values that are defined
if (arg.mpvReuseInstance !== undefined) {
store.set(MPV_REUSE_INSTANCE, arg.mpvReuseInstance);
}
// Handle remote control settings
if (arg.remoteControl !== undefined || arg.remoteControlPort !== undefined) {
const enabled = arg.remoteControl ?? store.get('remoteControl', false);
const port = arg.remoteControlPort ?? store.get('remoteControlPort', 8765);
// Save to store
if (arg.remoteControl !== undefined) {
store.set('remoteControl', enabled);
}
if (arg.remoteControlPort !== undefined) {
store.set('remoteControlPort', port);
}
// Update HTTP server
httpServer.updateSettings(enabled, port);
}
});
@@ -0,0 +1,200 @@
import { app } from 'electron';
import * as fs from 'fs';
import * as http from 'http';
import * as path from 'path';
/**
* HTTP server for serving the remote control web app and providing REST API endpoints
*/
export class HttpServer {
private server: http.Server | null = null;
private port = 8765;
private isEnabled = false;
private distPath: string;
private remoteControlHandlers: Map<
string,
(req: http.IncomingMessage, res: http.ServerResponse) => void
> = new Map();
constructor() {
// Path to the built remote-control-web app
// In development: use workspace root
// In production: use app path
const appPath = app.getAppPath();
const isDev = !app.isPackaged;
if (isDev) {
// Development mode - use workspace root
this.distPath = path.join(
process.cwd(),
'dist',
'apps',
'remote-control-web',
'browser'
);
} else {
// Production mode - files are bundled with the app
this.distPath = path.join(
appPath,
'dist',
'apps',
'remote-control-web',
'browser'
);
}
console.log('[HTTP Server] Serving from:', this.distPath);
}
/**
* Start the HTTP server
*/
start(port?: number): void {
if (port) {
this.port = port;
}
if (this.server) {
console.log('HTTP server is already running');
return;
}
this.server = http.createServer((req, res) => {
this.handleRequest(req, res);
});
this.server.listen(this.port, () => {
console.log(`HTTP server listening on port ${this.port}`);
console.log(
`Remote control available at: http://localhost:${this.port}`
);
});
this.isEnabled = true;
}
/**
* Stop the HTTP server
*/
stop(): void {
if (!this.server) {
return;
}
this.server.close(() => {
console.log('HTTP server stopped');
});
this.server = null;
this.isEnabled = false;
}
/**
* Update server settings
*/
updateSettings(enabled: boolean, port: number): void {
const needsRestart = this.isEnabled && enabled && this.port !== port;
if (!enabled && this.isEnabled) {
this.stop();
} else if (enabled && !this.isEnabled) {
this.start(port);
} else if (needsRestart) {
this.stop();
this.start(port);
}
}
/**
* Register a handler for remote control API endpoints
*/
registerRemoteControlHandler(
path: string,
handler: (req: http.IncomingMessage, res: http.ServerResponse) => void
): void {
this.remoteControlHandlers.set(path, handler);
}
/**
* Handle incoming HTTP requests
*/
private handleRequest(
req: http.IncomingMessage,
res: http.ServerResponse
): void {
const url = req.url || '/';
// Handle API requests
if (url.startsWith('/api/remote-control/')) {
const handler = this.remoteControlHandlers.get(url);
if (handler) {
handler(req, res);
return;
}
res.writeHead(404, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: 'Endpoint not found' }));
return;
}
// Serve static files from the remote-control-web app
this.serveStaticFile(url, res);
}
/**
* Serve static files
*/
private serveStaticFile(url: string, res: http.ServerResponse): void {
// Default to index.html for root path
let filePath = url === '/' ? '/index.html' : url;
// Security: prevent directory traversal
filePath = path.normalize(filePath).replace(/^(\.\.[/\\])+/, '');
const fullPath = path.join(this.distPath, filePath);
fs.readFile(fullPath, (err, data) => {
if (err) {
// If file not found, try serving index.html (for Angular routing)
if (err.code === 'ENOENT' && filePath !== '/index.html') {
this.serveStaticFile('/', res);
return;
}
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('404 Not Found');
return;
}
// Determine content type
const contentType = this.getContentType(fullPath);
res.writeHead(200, { 'Content-Type': contentType });
res.end(data);
});
}
/**
* Get content type based on file extension
*/
private getContentType(filePath: string): string {
const ext = path.extname(filePath).toLowerCase();
const contentTypes: Record<string, string> = {
'.html': 'text/html',
'.js': 'application/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.ico': 'image/x-icon',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
};
return contentTypes[ext] || 'application/octet-stream';
}
}
export const httpServer = new HttpServer();
+2
View File
@@ -7,6 +7,7 @@ import ElectronEvents from './app/events/electron.events';
import EpgEvents from './app/events/epg.events';
import PlayerEvents from './app/events/player.events';
import PlaylistEvents from './app/events/playlist.events';
import RemoteControlEvents from './app/events/remote-control.events';
import SettingsEvents from './app/events/settings.events';
import SharedEvents from './app/events/shared.events';
import SquirrelEvents from './app/events/squirrel.events';
@@ -40,6 +41,7 @@ export default class Main {
XtreamEvents.bootstrapXtreamEvents();
DatabaseEvents.bootstrapDatabaseEvents();
EpgEvents.bootstrapEpgEvents();
RemoteControlEvents.bootstrapRemoteControlEvents();
// initialize auto updater service
if (!App.isDevelopmentMode()) {
+174
View File
@@ -0,0 +1,174 @@
# Remote Control Web App
A lightweight, mobile-optimized web application that provides remote control functionality for IPTVnator. Control channel switching from your phone or tablet on the same network.
## Overview
This is a standalone Angular application designed to be served by IPTVnator's Electron backend via an HTTP server. It provides a beautiful, Apple TV-inspired remote control interface that communicates with the main IPTVnator desktop application to change channels.
## Features
- **Apple TV-Inspired Design** - Premium dark theme with glass morphism effects
- **Mobile-Optimized** - Touch-friendly interface designed for phones and tablets
- **Real-time Control** - Instant channel switching via REST API
- **Minimal & Fast** - Lightweight app (~55 KB compressed) with no external dependencies
- **Responsive** - Adapts to different screen sizes (320px - 480px+)
## How It Works
### Architecture
```
┌─────────────────┐ HTTP ┌──────────────────┐
│ Phone/Tablet │ ◄──────────────────► │ HTTP Server │
│ (Browser App) │ REST API calls │ (Electron Main) │
└─────────────────┘ └──────────────────┘
│ IPC
▼
┌──────────────────┐
│ Main App │
│ (Renderer) │
└──────────────────┘
```
1. **HTTP Server**: Electron backend starts an HTTP server (default port: 8765)
2. **Static Files**: Serves this Angular app's built files
3. **REST API**: Exposes `/api/remote-control/channel/up` and `/api/remote-control/channel/down` endpoints
4. **IPC Communication**: HTTP server sends events to the main Electron window
5. **Channel Switching**: Main app receives events and switches to next/previous channel
### API Endpoints
- `GET /` - Serves the remote control web interface
- `POST /api/remote-control/channel/up` - Switch to previous channel
- `POST /api/remote-control/channel/down` - Switch to next channel
## Usage
### Enable Remote Control
1. Open IPTVnator desktop app
2. Go to **Settings**
3. Enable **"Remote Control"** checkbox
4. Optionally change the port (default: 8765)
5. The HTTP server starts automatically
### Access from Mobile Device
1. Ensure your phone/tablet is on the **same network** as your computer
2. Find your computer's IP address:
- **Windows**: `ipconfig` (look for IPv4)
- **macOS**: `ifconfig` or System Preferences → Network
- **Linux**: `ip addr` or `hostname -I`
3. Open browser on your mobile device
4. Navigate to: `http://<YOUR_IP>:8765`
- Example: `http://192.168.1.100:8765`
### Controls
- **Top half of circle**: Previous channel (Channel Up)
- **Bottom half of circle**: Next channel (Channel Down)
- **Center blue dot**: Status indicator (glowing = ready)
## Development
### Build
```bash
# Production build
nx build remote-control-web --configuration=production
# Development build
nx build remote-control-web
```
Built files are output to `dist/apps/remote-control-web/browser/`
### Serve Locally
The app is designed to be served by the Electron backend's HTTP server. To test:
```bash
# Start Electron backend (includes HTTP server)
nx serve electron-backend
# Access at http://localhost:8765
```
### Project Structure
```
apps/remote-control-web/
├── src/
│ ├── app/
│ │ ├── app.component.ts # Root component
│ │ ├── app.config.ts # Angular configuration
│ │ └── app.routes.ts # Routes (single route)
│ ├── assets/ # Static assets (if any)
│ ├── index.html # HTML entry point
│ └── main.ts # Bootstrap Angular app
├── project.json # Nx project configuration
├── tsconfig.json # TypeScript config
└── README.md # This file
```
## Shared UI Library
The remote control component is located in:
```
libs/ui/remote-control/
├── src/lib/remote-control/
│ ├── remote-control.component.ts # Main component logic
│ ├── remote-control.component.html # Template
│ ├── remote-control.component.scss # Apple TV-inspired styles
│ └── remote-control.service.ts # HTTP service for API calls
```
This library is shared and can be reused in other applications if needed.
## Technical Details
- **Framework**: Angular 18+ (Standalone components)
- **HTTP Client**: Angular HttpClient
- **Styling**: SCSS with BEM methodology
- **Design System**: Apple TV-inspired with glass morphism
- **Animations**: CSS-only (no JavaScript animations)
- **Bundle Size**: ~55 KB gzipped (production build)
- **Browser Support**: Modern browsers (Chrome, Safari, Firefox, Edge)
## Design Philosophy
The interface is inspired by Apple TV's remote control:
- **Minimalism**: Clean, uncluttered design with essential controls only
- **Glass Morphism**: Frosted glass effects with subtle reflections
- **Touch-Optimized**: Large touch targets (50% of circle per button)
- **Feedback**: Visual feedback on hover/press with ripple effects
- **Accessibility**: Supports reduced motion preferences
## Security Considerations
- **Local Network Only**: The HTTP server should only be accessible on your local network
- **No Authentication**: Currently no authentication - ensure your network is trusted
- **CORS**: Server accepts requests from any origin on the local network
## Future Enhancements
Potential improvements:
- Volume control
- Play/pause functionality
- Channel search
- Favorites quick access
- Authentication/PIN protection
- WebSocket for real-time status updates
- Multi-device support with channel sync
## Related Files
- **HTTP Server**: `apps/electron-backend/src/app/server/http-server.ts`
- **Remote Control Events**: `apps/electron-backend/src/app/events/remote-control.events.ts`
- **Main App Handler**: `apps/web/src/app/home/video-player/video-player.component.ts` (handleRemoteChannelChange method)
- **Settings UI**: `apps/web/src/app/settings/settings.component.html`
## License
Same as IPTVnator main project.
+34
View File
@@ -0,0 +1,34 @@
import nx from '@nx/eslint-plugin';
import baseConfig from '../../eslint.config.mjs';
export default [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'app',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'app',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];
+21
View File
@@ -0,0 +1,21 @@
export default {
displayName: 'remote-control-web',
preset: '../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../coverage/apps/remote-control-web',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
},
],
},
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
};
+96
View File
@@ -0,0 +1,96 @@
{
"name": "remote-control-web",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"prefix": "app",
"sourceRoot": "apps/remote-control-web/src",
"tags": [],
"targets": {
"build": {
"executor": "@angular/build:application",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/remote-control-web",
"browser": "apps/remote-control-web/src/main.ts",
"polyfills": ["zone.js"],
"tsConfig": "apps/remote-control-web/tsconfig.app.json",
"inlineStyleLanguage": "scss",
"assets": [
{
"glob": "**/*",
"input": "apps/remote-control-web/public"
},
{
"glob": "**/*",
"input": "apps/web/src/assets/i18n",
"output": "assets/i18n"
}
],
"styles": ["apps/remote-control-web/src/styles.scss"]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kb",
"maximumError": "1mb"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kb",
"maximumError": "8kb"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"continuous": true,
"executor": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "remote-control-web:build:production"
},
"development": {
"buildTarget": "remote-control-web:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"executor": "@angular/build:extract-i18n",
"options": {
"buildTarget": "remote-control-web:build"
}
},
"lint": {
"executor": "@nx/eslint:lint"
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "apps/remote-control-web/jest.config.ts",
"tsConfig": "apps/remote-control-web/tsconfig.spec.json"
}
},
"serve-static": {
"continuous": true,
"executor": "@nx/web:file-server",
"options": {
"buildTarget": "remote-control-web:build",
"port": 4200,
"staticFilePath": "dist/apps/remote-control-web/browser",
"spa": true
}
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,32 @@
import {
ApplicationConfig,
importProvidersFrom,
provideBrowserGlobalErrorListeners,
provideZoneChangeDetection,
} from '@angular/core';
import { provideHttpClient } from '@angular/common/http';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { HttpClient } from '@angular/common/http';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideHttpClient(),
importProvidersFrom(
TranslateModule.forRoot({
defaultLanguage: 'en',
loader: {
provide: TranslateLoader,
useFactory: HttpLoaderFactory,
deps: [HttpClient],
},
})
),
],
};
+1
View File
@@ -0,0 +1 @@
<lib-remote-control />
+4
View File
@@ -0,0 +1,4 @@
:host {
display: block;
height: 100vh;
}
+19
View File
@@ -0,0 +1,19 @@
import { Component, OnInit, inject } from '@angular/core';
import { RemoteControlComponent } from 'remote-control';
import { TranslateService } from '@ngx-translate/core';
@Component({
imports: [RemoteControlComponent],
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.scss',
})
export class App implements OnInit {
private translate = inject(TranslateService);
ngOnInit() {
// Set default language
this.translate.setDefaultLang('en');
this.translate.use('en');
}
}
+13
View File
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>remote-control-web</title>
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
</head>
<body>
<app-root></app-root>
</body>
</html>
+5
View File
@@ -0,0 +1,5 @@
import { bootstrapApplication } from '@angular/platform-browser';
import { appConfig } from './app/app.config';
import { App } from './app/app';
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
+11
View File
@@ -0,0 +1,11 @@
/* You can add global styles to this file, and also import other style files */
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background-color: #1a1a1a;
overflow-x: hidden;
}
@@ -0,0 +1,6 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
setupZoneTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"types": []
},
"include": ["src/**/*.ts"],
"exclude": [
"jest.config.ts",
"src/test-setup.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts"
]
}
+32
View File
@@ -0,0 +1,32 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"target": "es2022",
"moduleResolution": "bundler",
"isolatedModules": true,
"emitDecoratorMetadata": false,
"module": "preserve"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
@@ -0,0 +1,17 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"target": "es2016",
"types": ["jest", "node"],
"moduleResolution": "node10"
},
"files": ["src/test-setup.ts"],
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
@@ -3,7 +3,6 @@ import { ComponentPortal } from '@angular/cdk/portal';
import { AsyncPipe, CommonModule } from '@angular/common';
import {
Component,
InjectionToken,
Injector,
OnInit,
effect,
@@ -17,6 +16,7 @@ import { StorageMap } from '@ngx-pwa/local-storage';
import {
ArtPlayerComponent,
AudioPlayerComponent,
COMPONENT_OVERLAY_REF,
EpgListComponent,
HtmlVideoPlayerComponent,
InfoOverlayComponent,
@@ -31,7 +31,7 @@ import {
selectChannels,
selectCurrentEpgProgram,
} from 'm3u-state';
import { Observable, combineLatestWith, filter, map, switchMap } from 'rxjs';
import { Observable, combineLatest, combineLatestWith, filter, map, switchMap, take } from 'rxjs';
import { DataService, PlaylistsService } from 'services';
import {
Channel,
@@ -73,10 +73,6 @@ export class VideoPlayerComponent implements OnInit {
private readonly storage = inject(StorageMap);
private readonly store = inject(Store);
private readonly COMPONENT_OVERLAY_REF = new InjectionToken<OverlayRef>(
'COMPONENT_OVERLAY_REF'
);
/** Active selected channel */
readonly activeChannel$ = this.store
.select(selectActive)
@@ -126,6 +122,13 @@ export class VideoPlayerComponent implements OnInit {
this.applySettings();
this.getPlaylistUrlAsParam();
// Setup remote control channel change listener (Electron only)
if (this.isDesktop && window.electron?.onChannelChange) {
window.electron.onChannelChange((data: { direction: 'up' | 'down' }) => {
this.handleRemoteChannelChange(data.direction);
});
}
this.channels$ = this.activatedRoute.params.pipe(
combineLatestWith(this.activatedRoute.queryParams),
switchMap(([params, queryParams]) => {
@@ -162,6 +165,56 @@ export class VideoPlayerComponent implements OnInit {
);
}
/**
* Handle remote control channel change
*/
handleRemoteChannelChange(direction: 'up' | 'down'): void {
console.log(`Remote control: changing channel ${direction}`);
// Use combineLatest to get both values and take only the first emission
combineLatest([this.channels$, this.activeChannel$])
.pipe(
filter(([channels, activeChannel]) => {
return channels.length > 0 && !!activeChannel;
}),
take(1),
map(([channels, activeChannel]) => {
return { channels, activeChannel: activeChannel as Channel };
})
)
.subscribe({
next: ({ channels, activeChannel }) => {
// Find current channel index
const currentIndex = channels.findIndex(
ch => ch.url === activeChannel.url
);
if (currentIndex === -1) {
console.warn('Current channel not found in channel list');
return;
}
// Calculate next/previous index with wraparound
let nextIndex: number;
if (direction === 'up') {
// Up = previous channel (decrease index)
nextIndex = currentIndex - 1 < 0 ? channels.length - 1 : currentIndex - 1;
} else {
// Down = next channel (increase index)
nextIndex = currentIndex + 1 >= channels.length ? 0 : currentIndex + 1;
}
// Dispatch action to change channel
const nextChannel = channels[nextIndex];
console.log(`Switching to channel: ${nextChannel.name}`);
this.store.dispatch(PlaylistActions.setActiveChannel({ channel: nextChannel }));
},
error: (err) => {
console.error('Error changing channel:', err);
}
});
}
/**
* Opens a playlist provided as a url param
* e.g. iptvnat.or?url=http://...
@@ -214,7 +267,7 @@ export class VideoPlayerComponent implements OnInit {
const injector = Injector.create({
providers: [
{
provide: this.COMPONENT_OVERLAY_REF,
provide: COMPONENT_OVERLAY_REF,
useValue: this.overlayRef,
},
],
@@ -27,7 +27,7 @@ const DEFAULT_SETTINGS: Settings = {
mpvReuseInstance: false,
vlcPlayerPath: '',
remoteControl: false,
remoteControlPort: 3000,
remoteControlPort: 8765,
epgUrl: [],
};
@@ -270,25 +270,23 @@
></mat-checkbox>
</div>
</div>
<!-- not ported to tauri yet
@if (isPwa || isDesktop) {
<mat-divider />
<div class="row">
<div class="column">
{{ 'SETTINGS.REMOTE_CONTROL' | translate }}
<p>
{{ 'SETTINGS.REMOTE_CONTROL_DESCRIPTION' | translate }}
</p>
@if (isDesktop) {
<mat-divider />
<div class="row">
<div class="column">
{{ 'SETTINGS.REMOTE_CONTROL' | translate }} (m3u)
<p>
{{ 'SETTINGS.REMOTE_CONTROL_DESCRIPTION' | translate }}
</p>
</div>
<div class="column margin-right">
<mat-checkbox
class="column"
formControlName="remoteControl"
></mat-checkbox>
</div>
</div>
<div class="column margin-right">
<mat-checkbox
class="column"
formControlName="remoteControl"
></mat-checkbox>
</div>
</div>
}
-->
@if (settingsForm.value.remoteControl === true) {
<mat-divider />
<div class="row">
@@ -302,13 +300,29 @@
</p>
</div>
<div class="column margin-right">
<mat-form-field appearance="outline" class="full-width">
<mat-form-field appearance="outline">
<mat-label>Port (1-65535)</mat-label>
<input
matInput
type="text"
type="number"
id="remoteControlPort"
formControlName="remoteControlPort"
min="1"
max="65535"
step="1"
/>
@if (settingsForm.get('remoteControlPort')?.hasError('required')) {
<mat-error>{{ 'SETTINGS.PORT_REQUIRED' | translate }}</mat-error>
}
@if (settingsForm.get('remoteControlPort')?.hasError('min')) {
<mat-error>{{ 'SETTINGS.PORT_MIN' | translate }}</mat-error>
}
@if (settingsForm.get('remoteControlPort')?.hasError('max')) {
<mat-error>{{ 'SETTINGS.PORT_MAX' | translate }}</mat-error>
}
@if (settingsForm.get('remoteControlPort')?.hasError('pattern')) {
<mat-error>{{ 'SETTINGS.PORT_PATTERN' | translate }}</mat-error>
}
</mat-form-field>
</div>
</div>
@@ -144,7 +144,15 @@ export class SettingsComponent implements OnInit {
mpvReuseInstance: false,
vlcPlayerPath: '',
remoteControl: false,
remoteControlPort: 3000,
remoteControlPort: [
8765,
[
Validators.required,
Validators.min(1),
Validators.max(65535),
Validators.pattern(/^\d+$/),
],
],
});
/** Form array with epg sources */
+379 -375
View File
@@ -1,377 +1,381 @@
{
"CLOSE": "Schließen",
"PLAYLISTS": "Playlisten",
"CHANNELS": {
"ALL_CHANNELS": "Alle Sender",
"FAVORITES": "Favoriten",
"GROUPS": "Gruppen",
"NO_FAVORITES": "Keine Favoriten",
"REMOVE_FAVORITE": "Aus Favoritenliste entfernen",
"SEARCH_CHANNEL": "Nach Sender suchen",
"UNGROUPED": "Ohne Gruppe",
"UNNAMED_CHANNEL": "Sender ohne Namen",
"UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Neue Playlist hochladen oder eine andere aussuchen",
"USE_STAR_TO_FAVORITE": "Nutzen Sie das Star-Icon um Sender als Favoriten zu markieren",
"FAVORITES_UPDATED": "Favoriten wurden aktualisiert!",
"SORT_BY": "Sortieren nach",
"SORT_BY_DATE": "Nach Datum sortieren (Neueste zuerst)",
"SORT_BY_RATING": "Nach Bewertung sortieren",
"SORT_BY_NAME": "Nach Name sortieren (A-Z)"
},
"EPG": {
"TITLE": "Elektronischer Programmführer",
"EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Versuchen Sie bitte die TV-Guide Einstellungen zu ändern.",
"EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Ups, leider ist das TV-Programm nicht verfügbar.",
"EPG_NOT_AVAILABLE_DATE": "Ups, leider ist das TV-Programm für das ausgewählte Datum nicht verfügbar",
"LIVE_NOW": "Live jetzt",
"LIVE_STREAM": "Live stream",
"NEXT_DAY": "Zum nächsten Tag",
"PREVIOUS_DAY": "Zum vorherigen Tag",
"TIMESHIFT_AVAILABLE": "Die Timeshift-Funktion (Archiv) ist verfügbar, klicken Sie an um abzuspielen",
"FETCH_EPG": "EPG-Daten werden geladen...",
"ERROR": "Ooops, EPG konnte nicht geladen werden",
"DOWNLOAD_SUCCESS": "EPG wurde erfolgreich geladen.",
"FETCH_SUCCESS": "EPG wurde erfolgreich abgerufen",
"PROGRAM_DIALOG": {
"PROGRAM_DETAILS": "Details zum TV-Programm",
"TITLE": "Titel",
"CATEGORY": "Kategorie",
"PARENTAL_RATING_SYSTEM": "Alterskennzeichnung (USK)",
"DESCRIPTION": "Beschreibung",
"LANGUAGE": "Sprache",
"SHOW_PROGRAM_DETAILS": "Details anzeigen"
"CLOSE": "Schließen",
"PLAYLISTS": "Playlisten",
"CHANNELS": {
"ALL_CHANNELS": "Alle Sender",
"FAVORITES": "Favoriten",
"GROUPS": "Gruppen",
"NO_FAVORITES": "Keine Favoriten",
"REMOVE_FAVORITE": "Aus Favoritenliste entfernen",
"SEARCH_CHANNEL": "Nach Sender suchen",
"UNGROUPED": "Ohne Gruppe",
"UNNAMED_CHANNEL": "Sender ohne Namen",
"UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Neue Playlist hochladen oder eine andere aussuchen",
"USE_STAR_TO_FAVORITE": "Nutzen Sie das Star-Icon um Sender als Favoriten zu markieren",
"FAVORITES_UPDATED": "Favoriten wurden aktualisiert!",
"SORT_BY": "Sortieren nach",
"SORT_BY_DATE": "Nach Datum sortieren (Neueste zuerst)",
"SORT_BY_RATING": "Nach Bewertung sortieren",
"SORT_BY_NAME": "Nach Name sortieren (A-Z)"
},
"EPG": {
"TITLE": "Elektronischer Programmführer",
"EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Versuchen Sie bitte die TV-Guide Einstellungen zu ändern.",
"EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Ups, leider ist das TV-Programm nicht verfügbar.",
"EPG_NOT_AVAILABLE_DATE": "Ups, leider ist das TV-Programm für das ausgewählte Datum nicht verfügbar",
"LIVE_NOW": "Live jetzt",
"LIVE_STREAM": "Live stream",
"NEXT_DAY": "Zum nächsten Tag",
"PREVIOUS_DAY": "Zum vorherigen Tag",
"TIMESHIFT_AVAILABLE": "Die Timeshift-Funktion (Archiv) ist verfügbar, klicken Sie an um abzuspielen",
"FETCH_EPG": "EPG-Daten werden geladen...",
"ERROR": "Ooops, EPG konnte nicht geladen werden",
"DOWNLOAD_SUCCESS": "EPG wurde erfolgreich geladen.",
"FETCH_SUCCESS": "EPG wurde erfolgreich abgerufen",
"PROGRAM_DIALOG": {
"PROGRAM_DETAILS": "Details zum TV-Programm",
"TITLE": "Titel",
"CATEGORY": "Kategorie",
"PARENTAL_RATING_SYSTEM": "Alterskennzeichnung (USK)",
"DESCRIPTION": "Beschreibung",
"LANGUAGE": "Sprache",
"SHOW_PROGRAM_DETAILS": "Details anzeigen"
}
},
"HOME": {
"SUBTITLE": "Wählen Sie eine Playlist aus oder laden Sie eine neue Playlist hoch",
"TITLE": "IPTVnator",
"FILE_UPLOAD": {
"CHOOSE_PLAYLIST": "Playlist auswählen",
"DRAG_DROP": "Ziehen Sie Dateien per Drag & Drop",
"OR": "oder",
"REJECTED": "Datei wurde abgelehnt, nicht unterstütztes Dateiformat {{filename}}."
},
"PLAYLISTS": {
"ADDED": "Hinzugefügt am",
"ADDED_VIA_FILE": "Diese Playlist wurde aus dem Dateisystem hochgeladen",
"ADDED_VIA_URL": "Diese Playlist wurde von der URL hinzugefügt",
"ADD_FIRST": "Bitte fügen Sie eine neue Playlist hinzu",
"CHANNELS": "TV-Sender",
"NO_PLAYLISTS": "Noch keine Playlisten hinzugefügt",
"REMOVE": "Playlist entfernen",
"SHOW_DETAILS": "Details anzeigen",
"SEARCH_PLAYLISTS": "Suche nach Playlisten",
"INFO_DIALOG": {
"CHANNELS": "Anzahl von Sender",
"CLOSE": "Schließen",
"FROM_URL": "Von der URL hinzugefügt",
"IMPORT_DATE": "Hinzugefügt am",
"PLAYLIST_DETAILS": "Infos über die Playlist",
"TITLE": "Titel",
"USER_AGENT": "User agent",
"SAVE": "Speichern",
"ORIGINAL_FILENAME": "Original-Dateinamen",
"FILE_PATH": "Dateipfad",
"UPDATE_FAILED": "Update fehlgeschlagen",
"AUTO_UPDATE": "Auto-update",
"AUTO_UPDATE_DESCRIPTION": "Wenn die Auto-Refresh-Funktion aktiviert ist, wird die Wiedergabeliste bei jedem Start der Anwendung automatisch aktualisiert.",
"CUSTOM_USER_AGENT": "Einige IPTV-Anbieter benötigen einen bestimmten 'User-Agent', um ihre Playlist abspielen zu können.",
"EXPORT_PLAYLIST": "Als m3u exportieren",
"PASSWORD": "Passwort",
"SERVER_URL": "Server-URL",
"USERNAME": "Nutzername",
"MAC_ADDRESS": "MAC-Adresse",
"PLAYLIST_EXPORT_SUCCESS": "Playlist wurde erfolgreich exportiert.",
"PLAYLIST_EXPORT_ERROR": "Fehler: Playlist-Export fehlgeschlagen."
},
"UPDATED": "Aktualisiert am",
"REFRESH": "Aktualisieren",
"REFRESH_XTREAM": "Xtream-Playlist vom Server aktualisieren",
"REFRESH_XTREAM_DIALOG": {
"TITLE": "Xtream-Playlist aktualisieren",
"MESSAGE": "Dadurch werden lokal zwischengespeicherte Kategorien und Streams gelöscht und alles vom Remote-Server erneut importiert. Ihre Favoriten und kürzlich angesehenen Elemente bleiben erhalten. Möchten Sie wirklich fortfahren?",
"STARTED": "Aktualisierung gestartet. Navigation zur Playlist...",
"ERROR": "Playlist-Aktualisierung fehlgeschlagen. Bitte versuchen Sie es erneut."
},
"REMOVE_DIALOG": {
"MESSAGE": "Sind Sie sicher, dass Sie diese Wiedergabeliste vollständig löschen möchten",
"TITLE": "Wiedergabeliste entfernen",
"SUCCESS": "Playlist wurde erfolgreich entfernt"
},
"GLOBAL_FAVORITES": "Globale Favoriten",
"GLOBAL_FAVORITES_DESCRIPTION": "Automatisch generierte Playlist mit aggregierten Favoriten aus allen Playlists",
"MY_PLAYLISTS": "Meine Playlisten",
"MY_PLAYLISTS_SUBTITLE": "Alle verfügbaren Playlisten",
"PLAYLIST_UPDATE_SUCCESS": "Erfolg! Die Playlist wurde erfolgreich aktualisiert.",
"PLAYLIST_UPDATE_ERROR": "Fehler beim Aktualisieren der Playlist-Details.",
"AUTO_REFRESH_UPDATE_SUCCESS": "Erfolg! Die Playlists wurden erfolgreich aktualisiert",
"AUTO_REFRESH_ENABLED": "Auto-Refresh aktiviert",
"STALKER_PORTAL": "Stalker-Portal",
"XTREAM_PLAYLIST": "Xtream-Playlist"
},
"TABS": {
"FILE_UPLOAD": "Aus Dateisystem hochladen",
"RECENTLY_ADDED": "Zuletzt hinzugefügt",
"URL_UPLOAD": "Via URL hinzufügen",
"TEXT_IMPORT": "Als Text importieren",
"XTREME_IMPORT": "Xtreme-Code hinzufügen",
"STALKER_PORTAL_IMPORT": "Stalker-Portal hinzufügen"
},
"URL_UPLOAD": {
"ADD_PLAYLIST": "Playlist hinzufügen",
"PLAYLIST_URL": "URL der Playlist",
"CORS_NOTE": "Hinweis: Um CORS-Probleme zu vermeiden, verwendet die Anwendung an dieser Stelle einen öffentlichen CORS-Proxy-Dienst. Wenn Sie eine Playlist mit sensiblen Daten hochladen möchten, ist es sicherer, sie aus einer Datei zu importieren."
},
"TEXT_IMPORT": {
"LABEL": "Füge m3u(8)-Wiedergabeliste hier als Text ein",
"BUTTON_LABEL": "Importieren"
},
"XTREAM_PLAYLIST": {
"TITLE": "Playlist Titel",
"SERVER_URL": "Server URL",
"USERNAME": "Nutzername",
"PASSWORD": "Passwort",
"ADD": "Hinzufügen",
"TEST_CREDENTIALS": "Verbindung testen",
"URL_VALIDATION_ERROR": "Sollte eine gültige URL mit Protokoll sein (z. B. http://example.com oder https://example.com:4324)"
},
"STALKER_PORTAL": {
"ADD": "Hinzufügen",
"TITLE": "Titel",
"SERVER_URL": "Server-URL",
"MAC_ADDRESS": "Mac-Adresse",
"URL_VALIDATION_ERROR": "Sollte eine gültige URL mit Protokoll sein (z. B. http://example.com/c oder https://example.com/stalker_portal/c)"
},
"FILTER_BY_TYPE": "Nach Typ filtern",
"FILTER_BY_NAME": "Nach Namen filtern",
"SORT_PLAYLISTS": "Playlisten sortieren",
"SORT_BY": "Sortieren nach",
"SORT_OPTIONS": {
"NAME_ASC": "Name (A-Z)",
"NAME_DESC": "Name (Z-A)",
"NEWEST": "Hinzugefügt (Neueste zuerst)",
"OLDEST": "Hinzugefügt (Älteste zuerst)"
},
"IMPORTED_AS_TEXT": "Als Text importiert",
"PARSING_ERROR": "Fehler: Parsen fehlgeschlagen, keine gültige Playlist:"
},
"LANGUAGES": {
"ARABIC": "العربية",
"MOROCCAN_ARABIC": "العربية (اللهجة المغربية)",
"ENGLISH": "English",
"KOREAN": "한국어",
"RUSSIAN": "Русский",
"GERMAN": "Deutsch",
"SPANISH": "Español",
"CHINESE": "简体中文",
"TRADITIONAL_CHINESE": "繁體中文",
"FRENCH": "Français",
"ITALIAN": "Italiano",
"TURKISH": "Türkçe",
"JAPANESE": "日本語",
"DUTCH": "Nederlands",
"BELARUSIAN": "Беларуская",
"POLISH": "Polski",
"PORTUGUESE": "Português"
},
"SETTINGS": {
"BACK_TO_HOME": "Zurück",
"DESCRIPTION": "Bitte vergessen Sie nicht die Änderungen mit dem Speicher-Button zu bestätigen",
"EPG_URL_LABEL": "TV-Programm (EPG) URL",
"EPG_URL_PLACEHOLDER": "URL eingeben",
"LANGUAGE": "Sprache",
"LATEST_VERSION": "Sie nutzen die neueste Version der Anwendung",
"NEW_VERSION_AVAILABLE": "EIne neue Version der Anwendung ist verfügbar",
"REFRESH_EPG": "TV-Programm aktualisieren",
"SAVE_CHANGES": "Änderungen speichern",
"SUBTITLE": "Die App-Einstellungen ändern",
"TITLE": "Einstellungen / IPTVnator",
"VERSION": "Version",
"VIDEO_PLAYER_LABEL": "Video player",
"VIDEO_PLAYER_PLACEHOLDER": "Option auswählen",
"STREAM_FORMAT": "Stream-Format",
"SETTINGS_SAVED": "Einstellungen wurden erfolgreich gespeichert!",
"THEME": "Farbdesign",
"SHOW_CAPTIONS": "Untertitel anzeigen",
"STREAM_FORMAT_DESCRIPTION": "Standard-Streamformat auswählen (Xtream)",
"REMOVE_ALL": "Playlist entfernen",
"REMOVE_ALL_BUTTON": "Entfernen",
"REMOVE_DIALOG": {
"TITLE": "Аlle Wiedergabelisten löschen",
"MESSAGE": "Sind Sie sicher, dass Sie alle Wiedergabelisten aus der Anwendung löschen möchten?"
},
"LANGUAGE_DESCRIPTION": "Wählen Sie die Sprache der Anwendung aus",
"PLAYLISTS_REMOVED": "Alle Playlists wurden entfernt.",
"VERSION_DESCRIPTION": "Aktuelle Version der Anwendung",
"REMOVE_ALL_DESCRIPTION": "Dadurch werden alle vorhandenen Wiedergabelisten aus der Anwendung entfernt.",
"MPV_PLAYER_PATH": "MPV-Player-Pfad",
"SHOW_CAPTIONS_DESCRIPTION": "Untertitel standardmäßig ein- oder ausblenden",
"IMPORT_DATA": "Import",
"ADD_EPG_SOURCE": "EPG-Quelle hinzufügen",
"IMPORT_ERROR": "Importfehler. Bitte versuchen Sie es mit einer anderen Datei.",
"EPG_URL_ERROR": "Der Wert ist keine gültige URL",
"EXPORT_DATA": "Export",
"VIDEO_PLAYER_DESCRIPTION": "Wählen Sie den Standard-Videoplayer aus",
"IMPORT_EXPORT_DATA": "Wiedergabelisten importieren/exportieren",
"VLC_PLAYER_PATH_LABEL": "VLC-Pfad",
"VLC_PLAYER_PATH_DESCRIPTION": "Legen Sie den Pfad zum VLC-Player in Ihrem System fest. \nEs wird der bereitgestellte Pfad zu einer VLC-Binärdatei anstelle des Standardpfads für unser System verwendet.",
"THEME_DESCRIPTION": "Wählen Sie das visuelle Thema der Anwendung aus",
"EPG_NOTE": "Derzeit ist EPG nur in der Electron-basierten Version von IPTVnator verfügbar.",
"VLC_PLAYER_PATH": "VLC-Playerpfad",
"IMPORT_EXPORT_DATA_DESCRIPTION": "Exportieren oder importieren Sie Wiedergabelisten im JSON-Format, das in der Anwendung verwendet wird.",
"EPG_NOTE_URL_TEXT": "Überprüfen Sie es hier",
"MPV_PLAYER_PATH_LABEL": "MPV-Pfad",
"MPV_PLAYER_PATH_DESCRIPTION": "Legen Sie den Pfad zum MPV-Player in Ihrem System fest. Es wird der bereitgestellte Pfad zu einer MPV-Binärdatei anstelle des in $PATH gefundenen verwendet.",
"MPV_REUSE_INSTANCE_LABEL": "MPV-Instanz wiederverwenden",
"MPV_REUSE_INSTANCE_DESCRIPTION": "Wenn aktiviert, wird dasselbe MPV-Fenster zum Abspielen verschiedener Streams wiederverwendet. Wenn deaktiviert, wird jeder Stream in einem neuen MPV-Fenster geöffnet.",
"REMOTE_CONTROL": "Fernsteuerung",
"REMOTE_CONTROL_DESCRIPTION": "Fernsteuerung der Anwendung aktivieren.",
"REMOTE_CONTROL_PORT": "Fernsteuerungsport",
"REMOTE_CONTROL_PORT_DESCRIPTION": "Port für die Fernsteuerung der Anwendung. Standardmäßig wird Port 8765 verwendet.",
"PORT_REQUIRED": "Port ist erforderlich",
"PORT_MIN": "Port muss mindestens 1 sein",
"PORT_MAX": "Port darf maximal 65535 sein",
"PORT_PATTERN": "Nur Ziffern sind erlaubt"
},
"THEMES": {
"DARK_THEME": "Dunkel",
"LIGHT_THEME": "Hell"
},
"TOP_MENU": {
"OPEN_CHANNELS_LIST": "Senderliste öffnen",
"OPEN_EPG_LIST": "TV-Guide (EPG) öffnen",
"TOGGLE_FAVORITE_FLAG": "Als Favoriten hinzufügen/entfernen",
"OPEN_MULTI_EPG": "Open Multi-EPG view"
},
"NO": "Nein",
"YES": "Ja",
"MENU": {
"OPEN": "Menü öffnen",
"SUPPORT_ARIA": "IPTVnator auf GitHub unterstützen",
"SETTINGS": "Einstellungen",
"SETTINGS_ARIA": "Einstellungen öffnen",
"BUG_REPORT": "Fehler melden",
"WHAT_IS_NEW": "Was ist neu",
"ABOUT": "Über",
"ABOUT_ARIA": "Über die Anwendung",
"SUPPORT_PROJECT": "Projekt unterstützen"
},
"XTREAM": {
"RELEASE_DATE": "Veröffentlichungsdatum",
"ACTORS": "Schauspieler",
"PLAY": "Abspielen",
"OPENING_PLAYER": "Player wird geöffnet...",
"GENRE": "Genre",
"DURATION": "Dauer",
"COUNTRY": "Land",
"NAME": "Name",
"DIRECTOR": "Regisseur",
"CAST": "Gießen",
"RATING": "Bewertung",
"AGE": "Alter",
"EPISODE": "Folge",
"YEAR": "Jahr",
"YOUTUBE_TRAILER": "YouTube-Trailer",
"DESCRIPTION": "Beschreibung",
"KINOPOISK_RATING": "Bewertung Kinopoisk",
"IMDB_RATING": "IMDB-Bewertung",
"EPISODE_RUN_TIME": "Laufzeit der Episode",
"ACCOUNT_INFO": {
"TITLE": "Kontoinformationen",
"USER_INFO": "Benutzerinformationen",
"STATUS": "Status",
"USERNAME": "Benutzername",
"ACTIVE_CONNECTIONS": "Aktive Verbindungen",
"CREATED": "Erstellt",
"EXPIRES": "Läuft ab",
"TRIAL_ACCOUNT": "Testkonto",
"YES": "Ja",
"NO": "Nein",
"ALLOWED_FORMATS": "Zulässige Formate",
"SERVER_INFO": "Serverinformationen",
"LIVE_TV": "Live-TV",
"MOVIES": "Filme",
"TV_SERIES": "Serien",
"URL": "URL",
"PROTOCOL": "Protokoll",
"TIMEZONE": "Zeitzone",
"SERVER_TIME": "Serverzeit",
"PORTS": "Ports",
"HTTP_PORT": "HTTP",
"HTTPS_PORT": "HTTPS",
"RTMP_PORT": "RTMP"
}
},
"PORTALS": {
"SIDEBAR": {
"MAIN": "Allgemein",
"MOVIES": "Filme",
"LIVE_TV": "Live TV",
"SERIES": "Serien",
"LIBRARY": "Bibliothek",
"SEARCH": "Suche",
"RECENT": "Zuletzt angesehen",
"FAVORITES": "Favoriten",
"HOME": "Startseite",
"PLAYLIST_INFO": "Playlist-Info",
"ACCOUNT_INFO": "Account-Info"
},
"ALL_CATEGORIES": "Alle Kategorien",
"ADD_TO_FAVORITES": "Zu den Favoriten hinzufügen",
"REMOVE_FROM_FAVORITES": "Von Favoriten entfernen",
"MY_FAVORITES": "Meine Favoriten",
"COPY_STREAM_URL": "Stream-URL kopieren",
"ERROR_VIEW": {
"DELETE": "Löschen",
"ACCOUNT_EXPIRED": {
"TITLE": "Konto abgelaufen",
"DESCRIPTION": "Dieses Konto ist abgelaufen. \nBitte überprüfen Sie die Anmeldeinformationen in den Playlist-Einstellungen oder entfernen Sie sie."
},
"HOME": "Startseite",
"EMPTY_CATEGORY": {
"TITLE": "Leere Kategorie",
"DESCRIPTION": "Diese Kategorie hat keinen Inhalt. Bitte wählen Sie eine andere Kategorie aus oder ändern Sie den Inhaltstyp"
},
"NO_CATEGORY_SELECTED": {
"TITLE": "Keine Kategorie ausgewählt",
"DESCRIPTION": "Bitte wählen Sie eine Kategorie aus, um den Inhalt anzuzeigen"
},
"UNAUTHORIZED": {
"TITLE": "Nicht autorisiert",
"DESCRIPTION": "Bitte überprüfen Sie die Anmeldeinformationen in den Playlist-Einstellungen oder entfernen Sie sie."
},
"NOT_FOUND": {
"TITLE": "Portal nicht gefunden",
"DESCRIPTION": "Bitte überprüfen Sie die Anmeldeinformationen und die Portal-URL in den Playlist-Einstellungen oder entfernen Sie sie."
},
"UNKNOWN_ERROR": {
"TITLE": "Etwas ist schief gelaufen",
"DESCRIPTION": "Unbekannter Fehler, bitte versuchen Sie es später noch einmal"
},
"PLAYLIST_SETTINGS": "Playlist-Einstellungen"
},
"STREAM_URL_COPIED": "Stream-URL in die Zwischenablage kopiert",
"EMPTY_LIST_VIEW": {
"TITLE": "Keine Ergebnisse gefunden",
"NO_SEARCH_RESULTS": "Nichts gefunden. Versuchen Sie, Ihre Suchanfrage zu ändern"
},
"REMOVED_FROM_FAVORITES": "Aus den Favoriten entfernt",
"ADDED_TO_FAVORITES": "Zu den Favoriten hinzugefügt",
"SELECT_CATEGORY": "Wählen Sie eine Kategorie aus",
"PAGE": "Seite"
},
"ABOUT": {
"TITLE": "Über IPTVnator",
"DESCRIPTION": "Plattformübergreifende IPTV-Player-Anwendung mit mehreren Funktionen wie Unterstützung von M3U- und M3U8-Wiedergabelisten, Favoriten, TV-Guide, TV-Archiv/Catchup und mehr.",
"VERSION": "Version",
"TWITTER_TOOLTIP": "Auf Twitter teilen",
"GITHUB_TOOLTIP": "ITPVnator-Repository auf GitHub",
"BUY_ME_A_COFFEE_TOOLTIP": "Buy me a coffee"
},
"REFRESH": "Aktualisieren",
"UPDATE_AVAILABLE": "Update verfügbar",
"INFORMATION": "Information",
"REMOTE_CONTROL": {
"TITLE": "Fernbedienung",
"UP_CHANNEL": "Sender aufwärts",
"DOWN_CHANNEL": "Sender abwärts",
"HEADER": "IPTVNator Fernbedienung",
"FOOTER": "IPTVNator Fernbedienung"
}
},
"HOME": {
"SUBTITLE": "Wählen Sie eine Playlist aus oder laden Sie eine neue Playlist hoch",
"TITLE": "IPTVnator",
"FILE_UPLOAD": {
"CHOOSE_PLAYLIST": "Playlist auswählen",
"DRAG_DROP": "Ziehen Sie Dateien per Drag & Drop",
"OR": "oder",
"REJECTED": "Datei wurde abgelehnt, nicht unterstütztes Dateiformat {{filename}}."
},
"PLAYLISTS": {
"ADDED": "Hinzugefügt am",
"ADDED_VIA_FILE": "Diese Playlist wurde aus dem Dateisystem hochgeladen",
"ADDED_VIA_URL": "Diese Playlist wurde von der URL hinzugefügt",
"ADD_FIRST": "Bitte fügen Sie eine neue Playlist hinzu",
"CHANNELS": "TV-Sender",
"NO_PLAYLISTS": "Noch keine Playlisten hinzugefügt",
"REMOVE": "Playlist entfernen",
"SHOW_DETAILS": "Details anzeigen",
"SEARCH_PLAYLISTS": "Suche nach Playlisten",
"INFO_DIALOG": {
"CHANNELS": "Anzahl von Sender",
"CLOSE": "Schließen",
"FROM_URL": "Von der URL hinzugefügt",
"IMPORT_DATE": "Hinzugefügt am",
"PLAYLIST_DETAILS": "Infos über die Playlist",
"TITLE": "Titel",
"USER_AGENT": "User agent",
"SAVE": "Speichern",
"ORIGINAL_FILENAME": "Original-Dateinamen",
"FILE_PATH": "Dateipfad",
"UPDATE_FAILED": "Update fehlgeschlagen",
"AUTO_UPDATE": "Auto-update",
"AUTO_UPDATE_DESCRIPTION": "Wenn die Auto-Refresh-Funktion aktiviert ist, wird die Wiedergabeliste bei jedem Start der Anwendung automatisch aktualisiert.",
"CUSTOM_USER_AGENT": "Einige IPTV-Anbieter benötigen einen bestimmten 'User-Agent', um ihre Playlist abspielen zu können.",
"EXPORT_PLAYLIST": "Als m3u exportieren",
"PASSWORD": "Passwort",
"SERVER_URL": "Server-URL",
"USERNAME": "Nutzername",
"MAC_ADDRESS": "MAC-Adresse",
"PLAYLIST_EXPORT_SUCCESS": "Playlist wurde erfolgreich exportiert.",
"PLAYLIST_EXPORT_ERROR": "Fehler: Playlist-Export fehlgeschlagen."
},
"UPDATED": "Aktualisiert am",
"REFRESH": "Aktualisieren",
"REFRESH_XTREAM": "Xtream-Playlist vom Server aktualisieren",
"REFRESH_XTREAM_DIALOG": {
"TITLE": "Xtream-Playlist aktualisieren",
"MESSAGE": "Dadurch werden lokal zwischengespeicherte Kategorien und Streams gelöscht und alles vom Remote-Server erneut importiert. Ihre Favoriten und kürzlich angesehenen Elemente bleiben erhalten. Möchten Sie wirklich fortfahren?",
"STARTED": "Aktualisierung gestartet. Navigation zur Playlist...",
"ERROR": "Playlist-Aktualisierung fehlgeschlagen. Bitte versuchen Sie es erneut."
},
"REMOVE_DIALOG": {
"MESSAGE": "Sind Sie sicher, dass Sie diese Wiedergabeliste vollständig löschen möchten",
"TITLE": "Wiedergabeliste entfernen",
"SUCCESS": "Playlist wurde erfolgreich entfernt"
},
"GLOBAL_FAVORITES": "Globale Favoriten",
"GLOBAL_FAVORITES_DESCRIPTION": "Automatisch generierte Playlist mit aggregierten Favoriten aus allen Playlists",
"MY_PLAYLISTS": "Meine Playlisten",
"MY_PLAYLISTS_SUBTITLE": "Alle verfügbaren Playlisten",
"PLAYLIST_UPDATE_SUCCESS": "Erfolg! Die Playlist wurde erfolgreich aktualisiert.",
"PLAYLIST_UPDATE_ERROR": "Fehler beim Aktualisieren der Playlist-Details.",
"AUTO_REFRESH_UPDATE_SUCCESS": "Erfolg! Die Playlists wurden erfolgreich aktualisiert",
"AUTO_REFRESH_ENABLED": "Auto-Refresh aktiviert",
"STALKER_PORTAL": "Stalker-Portal",
"XTREAM_PLAYLIST": "Xtream-Playlist"
},
"TABS": {
"FILE_UPLOAD": "Aus Dateisystem hochladen",
"RECENTLY_ADDED": "Zuletzt hinzugefügt",
"URL_UPLOAD": "Via URL hinzufügen",
"TEXT_IMPORT": "Als Text importieren",
"XTREME_IMPORT": "Xtreme-Code hinzufügen",
"STALKER_PORTAL_IMPORT": "Stalker-Portal hinzufügen"
},
"URL_UPLOAD": {
"ADD_PLAYLIST": "Playlist hinzufügen",
"PLAYLIST_URL": "URL der Playlist",
"CORS_NOTE": "Hinweis: Um CORS-Probleme zu vermeiden, verwendet die Anwendung an dieser Stelle einen öffentlichen CORS-Proxy-Dienst. Wenn Sie eine Playlist mit sensiblen Daten hochladen möchten, ist es sicherer, sie aus einer Datei zu importieren."
},
"TEXT_IMPORT": {
"LABEL": "Füge m3u(8)-Wiedergabeliste hier als Text ein",
"BUTTON_LABEL": "Importieren"
},
"XTREAM_PLAYLIST": {
"TITLE": "Playlist Titel",
"SERVER_URL": "Server URL",
"USERNAME": "Nutzername",
"PASSWORD": "Passwort",
"ADD": "Hinzufügen",
"TEST_CREDENTIALS": "Verbindung testen",
"URL_VALIDATION_ERROR": "Sollte eine gültige URL mit Protokoll sein (z. B. http://example.com oder https://example.com:4324)"
},
"STALKER_PORTAL": {
"ADD": "Hinzufügen",
"TITLE": "Titel",
"SERVER_URL": "Server-URL",
"MAC_ADDRESS": "Mac-Adresse",
"URL_VALIDATION_ERROR": "Sollte eine gültige URL mit Protokoll sein (z. B. http://example.com/c oder https://example.com/stalker_portal/c)"
},
"FILTER_BY_TYPE": "Nach Typ filtern",
"FILTER_BY_NAME": "Nach Namen filtern",
"SORT_PLAYLISTS": "Playlisten sortieren",
"SORT_BY": "Sortieren nach",
"SORT_OPTIONS": {
"NAME_ASC": "Name (A-Z)",
"NAME_DESC": "Name (Z-A)",
"NEWEST": "Hinzugefügt (Neueste zuerst)",
"OLDEST": "Hinzugefügt (Älteste zuerst)"
},
"IMPORTED_AS_TEXT": "Als Text importiert",
"PARSING_ERROR": "Fehler: Parsen fehlgeschlagen, keine gültige Playlist:"
},
"LANGUAGES": {
"ARABIC": "العربية",
"MOROCCAN_ARABIC": "العربية (اللهجة المغربية)",
"ENGLISH": "English",
"KOREAN": "한국어",
"RUSSIAN": "Русский",
"GERMAN": "Deutsch",
"SPANISH": "Español",
"CHINESE": "简体中文",
"TRADITIONAL_CHINESE": "繁體中文",
"FRENCH": "Français",
"ITALIAN": "Italiano",
"TURKISH": "Türkçe",
"JAPANESE": "日本語",
"DUTCH": "Nederlands",
"BELARUSIAN": "Беларуская",
"POLISH": "Polski",
"PORTUGUESE": "Português"
},
"SETTINGS": {
"BACK_TO_HOME": "Zurück",
"DESCRIPTION": "Bitte vergessen Sie nicht die Änderungen mit dem Speicher-Button zu bestätigen",
"EPG_URL_LABEL": "TV-Programm (EPG) URL",
"EPG_URL_PLACEHOLDER": "URL eingeben",
"LANGUAGE": "Sprache",
"LATEST_VERSION": "Sie nutzen die neueste Version der Anwendung",
"NEW_VERSION_AVAILABLE": "EIne neue Version der Anwendung ist verfügbar",
"REFRESH_EPG": "TV-Programm aktualisieren",
"SAVE_CHANGES": "Änderungen speichern",
"SUBTITLE": "Die App-Einstellungen ändern",
"TITLE": "Einstellungen / IPTVnator",
"VERSION": "Version",
"VIDEO_PLAYER_LABEL": "Video player",
"VIDEO_PLAYER_PLACEHOLDER": "Option auswählen",
"STREAM_FORMAT": "Stream-Format",
"SETTINGS_SAVED": "Einstellungen wurden erfolgreich gespeichert!",
"THEME": "Farbdesign",
"SHOW_CAPTIONS": "Untertitel anzeigen",
"STREAM_FORMAT_DESCRIPTION": "Standard-Streamformat auswählen (Xtream)",
"REMOVE_ALL": "Playlist entfernen",
"REMOVE_ALL_BUTTON": "Entfernen",
"REMOVE_DIALOG": {
"TITLE": "Аlle Wiedergabelisten löschen",
"MESSAGE": "Sind Sie sicher, dass Sie alle Wiedergabelisten aus der Anwendung löschen möchten?"
},
"LANGUAGE_DESCRIPTION": "Wählen Sie die Sprache der Anwendung aus",
"PLAYLISTS_REMOVED": "Alle Playlists wurden entfernt.",
"VERSION_DESCRIPTION": "Aktuelle Version der Anwendung",
"REMOVE_ALL_DESCRIPTION": "Dadurch werden alle vorhandenen Wiedergabelisten aus der Anwendung entfernt.",
"MPV_PLAYER_PATH": "MPV-Player-Pfad",
"SHOW_CAPTIONS_DESCRIPTION": "Untertitel standardmäßig ein- oder ausblenden",
"IMPORT_DATA": "Import",
"ADD_EPG_SOURCE": "EPG-Quelle hinzufügen",
"IMPORT_ERROR": "Importfehler. Bitte versuchen Sie es mit einer anderen Datei.",
"EPG_URL_ERROR": "Der Wert ist keine gültige URL",
"EXPORT_DATA": "Export",
"VIDEO_PLAYER_DESCRIPTION": "Wählen Sie den Standard-Videoplayer aus",
"IMPORT_EXPORT_DATA": "Wiedergabelisten importieren/exportieren",
"VLC_PLAYER_PATH_LABEL": "VLC-Pfad",
"VLC_PLAYER_PATH_DESCRIPTION": "Legen Sie den Pfad zum VLC-Player in Ihrem System fest. \nEs wird der bereitgestellte Pfad zu einer VLC-Binärdatei anstelle des Standardpfads für unser System verwendet.",
"THEME_DESCRIPTION": "Wählen Sie das visuelle Thema der Anwendung aus",
"EPG_NOTE": "Derzeit ist EPG nur in der Electron-basierten Version von IPTVnator verfügbar.",
"VLC_PLAYER_PATH": "VLC-Playerpfad",
"IMPORT_EXPORT_DATA_DESCRIPTION": "Exportieren oder importieren Sie Wiedergabelisten im JSON-Format, das in der Anwendung verwendet wird.",
"EPG_NOTE_URL_TEXT": "Überprüfen Sie es hier",
"MPV_PLAYER_PATH_LABEL": "MPV-Pfad",
"MPV_PLAYER_PATH_DESCRIPTION": "Legen Sie den Pfad zum MPV-Player in Ihrem System fest. Es wird der bereitgestellte Pfad zu einer MPV-Binärdatei anstelle des in $PATH gefundenen verwendet.",
"MPV_REUSE_INSTANCE_LABEL": "MPV-Instanz wiederverwenden",
"MPV_REUSE_INSTANCE_DESCRIPTION": "Wenn aktiviert, wird dasselbe MPV-Fenster zum Abspielen verschiedener Streams wiederverwendet. Wenn deaktiviert, wird jeder Stream in einem neuen MPV-Fenster geöffnet.",
"REMOTE_CONTROL": "Fernbedienung",
"REMOTE_CONTROL_DESCRIPTION": "Fernsteuerung der Anwendung aktivieren.",
"REMOTE_CONTROL_PORT": "Fernsteuerungs-Port",
"REMOTE_CONTROL_PORT_DESCRIPTION": "Port für die Fernsteuerung der Anwendung. Standardmäßig wird Port 3000 verwendet."
},
"THEMES": {
"DARK_THEME": "Dunkel",
"LIGHT_THEME": "Hell"
},
"TOP_MENU": {
"OPEN_CHANNELS_LIST": "Senderliste öffnen",
"OPEN_EPG_LIST": "TV-Guide (EPG) öffnen",
"TOGGLE_FAVORITE_FLAG": "Als Favoriten hinzufügen/entfernen",
"OPEN_MULTI_EPG": "Open Multi-EPG view"
},
"NO": "Nein",
"YES": "Ja",
"MENU": {
"OPEN": "Menü öffnen",
"SUPPORT_ARIA": "IPTVnator auf GitHub unterstützen",
"SETTINGS": "Einstellungen",
"SETTINGS_ARIA": "Einstellungen öffnen",
"BUG_REPORT": "Fehler melden",
"WHAT_IS_NEW": "Was ist neu",
"ABOUT": "Über",
"ABOUT_ARIA": "Über die Anwendung",
"SUPPORT_PROJECT": "Projekt unterstützen"
},
"XTREAM": {
"RELEASE_DATE": "Veröffentlichungsdatum",
"ACTORS": "Schauspieler",
"PLAY": "Abspielen",
"OPENING_PLAYER": "Player wird geöffnet...",
"GENRE": "Genre",
"DURATION": "Dauer",
"COUNTRY": "Land",
"NAME": "Name",
"DIRECTOR": "Regisseur",
"CAST": "Gießen",
"RATING": "Bewertung",
"AGE": "Alter",
"EPISODE": "Folge",
"YEAR": "Jahr",
"YOUTUBE_TRAILER": "YouTube-Trailer",
"DESCRIPTION": "Beschreibung",
"KINOPOISK_RATING": "Bewertung Kinopoisk",
"IMDB_RATING": "IMDB-Bewertung",
"EPISODE_RUN_TIME": "Laufzeit der Episode",
"ACCOUNT_INFO": {
"TITLE": "Kontoinformationen",
"USER_INFO": "Benutzerinformationen",
"STATUS": "Status",
"USERNAME": "Benutzername",
"ACTIVE_CONNECTIONS": "Aktive Verbindungen",
"CREATED": "Erstellt",
"EXPIRES": "Läuft ab",
"TRIAL_ACCOUNT": "Testkonto",
"YES": "Ja",
"NO": "Nein",
"ALLOWED_FORMATS": "Zulässige Formate",
"SERVER_INFO": "Serverinformationen",
"LIVE_TV": "Live-TV",
"MOVIES": "Filme",
"TV_SERIES": "Serien",
"URL": "URL",
"PROTOCOL": "Protokoll",
"TIMEZONE": "Zeitzone",
"SERVER_TIME": "Serverzeit",
"PORTS": "Ports",
"HTTP_PORT": "HTTP",
"HTTPS_PORT": "HTTPS",
"RTMP_PORT": "RTMP"
}
},
"PORTALS": {
"SIDEBAR": {
"MAIN": "Allgemein",
"MOVIES": "Filme",
"LIVE_TV": "Live TV",
"SERIES": "Serien",
"LIBRARY": "Bibliothek",
"SEARCH": "Suche",
"RECENT": "Zuletzt angesehen",
"FAVORITES": "Favoriten",
"HOME": "Startseite",
"PLAYLIST_INFO": "Playlist-Info",
"ACCOUNT_INFO": "Account-Info"
},
"ALL_CATEGORIES": "Alle Kategorien",
"ADD_TO_FAVORITES": "Zu den Favoriten hinzufügen",
"REMOVE_FROM_FAVORITES": "Von Favoriten entfernen",
"MY_FAVORITES": "Meine Favoriten",
"COPY_STREAM_URL": "Stream-URL kopieren",
"ERROR_VIEW": {
"DELETE": "Löschen",
"ACCOUNT_EXPIRED": {
"TITLE": "Konto abgelaufen",
"DESCRIPTION": "Dieses Konto ist abgelaufen. \nBitte überprüfen Sie die Anmeldeinformationen in den Playlist-Einstellungen oder entfernen Sie sie."
},
"HOME": "Startseite",
"EMPTY_CATEGORY": {
"TITLE": "Leere Kategorie",
"DESCRIPTION": "Diese Kategorie hat keinen Inhalt. Bitte wählen Sie eine andere Kategorie aus oder ändern Sie den Inhaltstyp"
},
"NO_CATEGORY_SELECTED": {
"TITLE": "Keine Kategorie ausgewählt",
"DESCRIPTION": "Bitte wählen Sie eine Kategorie aus, um den Inhalt anzuzeigen"
},
"UNAUTHORIZED": {
"TITLE": "Nicht autorisiert",
"DESCRIPTION": "Bitte überprüfen Sie die Anmeldeinformationen in den Playlist-Einstellungen oder entfernen Sie sie."
},
"NOT_FOUND": {
"TITLE": "Portal nicht gefunden",
"DESCRIPTION": "Bitte überprüfen Sie die Anmeldeinformationen und die Portal-URL in den Playlist-Einstellungen oder entfernen Sie sie."
},
"UNKNOWN_ERROR": {
"TITLE": "Etwas ist schief gelaufen",
"DESCRIPTION": "Unbekannter Fehler, bitte versuchen Sie es später noch einmal"
},
"PLAYLIST_SETTINGS": "Playlist-Einstellungen"
},
"STREAM_URL_COPIED": "Stream-URL in die Zwischenablage kopiert",
"EMPTY_LIST_VIEW": {
"TITLE": "Keine Ergebnisse gefunden",
"NO_SEARCH_RESULTS": "Nichts gefunden. Versuchen Sie, Ihre Suchanfrage zu ändern"
},
"REMOVED_FROM_FAVORITES": "Aus den Favoriten entfernt",
"ADDED_TO_FAVORITES": "Zu den Favoriten hinzugefügt",
"SELECT_CATEGORY": "Wählen Sie eine Kategorie aus",
"PAGE": "Seite"
},
"ABOUT": {
"TITLE": "Über IPTVnator",
"DESCRIPTION": "Plattformübergreifende IPTV-Player-Anwendung mit mehreren Funktionen wie Unterstützung von M3U- und M3U8-Wiedergabelisten, Favoriten, TV-Guide, TV-Archiv/Catchup und mehr.",
"VERSION": "Version",
"TWITTER_TOOLTIP": "Auf Twitter teilen",
"GITHUB_TOOLTIP": "ITPVnator-Repository auf GitHub",
"BUY_ME_A_COFFEE_TOOLTIP": "Buy me a coffee"
},
"REFRESH": "Aktualisieren",
"UPDATE_AVAILABLE": "Update verfügbar",
"INFORMATION": "Information",
"REMOTE_CONTROL": {
"TITLE": "Fernbedienung",
"UP_CHANNEL": "Sender aufwärts",
"DOWN_CHANNEL": "Sender abwärts",
"HEADER": "IPTVNator Fernbedienung",
"FOOTER": "IPTVNator Fernbedienung"
}
}
}
+8 -3
View File
@@ -167,7 +167,11 @@
"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."
"REMOTE_CONTROL_PORT_DESCRIPTION": "Port used for remote control of the application. Port 8765 is used by default.",
"PORT_REQUIRED": "Port is required",
"PORT_MIN": "Port must be at least 1",
"PORT_MAX": "Port must be at most 65535",
"PORT_PATTERN": "Only digits are allowed"
},
"THEMES": {
"DARK_THEME": "Dark theme",
@@ -369,8 +373,9 @@
"INFORMATION": "Information",
"REMOTE_CONTROL": {
"TITLE": "Remote control",
"UP_CHANNEL": "Up channel",
"DOWN_CHANNEL": "Down channel",
"SUBTITLE": "Control your IPTV player remotely",
"UP_CHANNEL": "Channel Up",
"DOWN_CHANNEL": "Channel Down",
"HEADER": "IPTVNator Remote Control",
"FOOTER": "IPTVNator Remote Control"
}
+370 -366
View File
@@ -1,377 +1,381 @@
{
"CLOSE": "Закрыть",
"PLAYLISTS": "Плейлисты",
"HOME": {
"TITLE": "IPTVnator",
"SUBTITLE": "Выберите плейлист из списка, добавьте новый с компьютера или загрузите по ссылке",
"TABS": {
"RECENTLY_ADDED": "Недавно добавленные плейлисты",
"FILE_UPLOAD": "Загрузить с компьютера",
"URL_UPLOAD": "Загрузить по ссылке",
"TEXT_IMPORT": "Импортировать из текста",
"STALKER_PORTAL_IMPORT": "Добавить Stalker портал",
"XTREME_IMPORT": "Добавить Xtreme Code "
"CLOSE": "Закрыть",
"PLAYLISTS": "Плейлисты",
"HOME": {
"TITLE": "IPTVnator",
"SUBTITLE": "Выберите плейлист из списка, добавьте новый с компьютера или загрузите по ссылке",
"TABS": {
"RECENTLY_ADDED": "Недавно добавленные плейлисты",
"FILE_UPLOAD": "Загрузить с компьютера",
"URL_UPLOAD": "Загрузить по ссылке",
"TEXT_IMPORT": "Импортировать из текста",
"STALKER_PORTAL_IMPORT": "Добавить Stalker портал",
"XTREME_IMPORT": "Добавить Xtreme Code "
},
"PLAYLISTS": {
"NO_PLAYLISTS": "Пока еще не было добавлено ни одного плейлиста",
"ADD_FIRST": "Вы можете добавить плейлисты из соседних вкладок",
"ADDED_VIA_URL": "Был добавлен по ссылке:",
"ADDED_VIA_FILE": "Был загружен с компьютера",
"CHANNELS": "Каналов",
"ADDED": "Добавлен",
"SHOW_DETAILS": "Показать информацию о плейлисте",
"REMOVE": "Удалить плейлист",
"SEARCH_PLAYLISTS": "Поиск по названию плейлиста",
"INFO_DIALOG": {
"PLAYLIST_DETAILS": "Подробная информация о плейлисте",
"TITLE": "Название",
"FROM_URL": "Добавлен по ссылке",
"IMPORT_DATE": "Дата импорта",
"CHANNELS": "Количество каналов",
"CLOSE": "Закрыть",
"SAVE": "Сохранить",
"USER_AGENT": "User agent",
"ORIGINAL_FILENAME": "Имя файла при загрузке",
"FILE_PATH": "Путь к файлу",
"UPDATE_FAILED": "Не удалось обновить плейлист",
"AUTO_UPDATE": "Автообновление",
"AUTO_UPDATE_DESCRIPTION": "Если активирована функция автообновления, список воспроизведения будет обновляться автоматически при каждом запуске приложения.",
"CUSTOM_USER_AGENT": "Некоторые IPTV провайдеры требуют указание определенного пользовательского агента (User Agent) для воспроизведения своего плейлиста.",
"EXPORT_PLAYLIST": "Экспортировать плейлист (m3u)",
"PASSWORD": "Пароль",
"SERVER_URL": "URL-адрес сервера",
"USERNAME": "Имя пользователя",
"MAC_ADDRESS": "MAC-адрес",
"PLAYLIST_EXPORT_SUCCESS": "Плейлист был успешно экспортирован.",
"PLAYLIST_EXPORT_ERROR": "Ошибка: не удалось экспортировать плейлист."
},
"UPDATED": "Обновлен",
"REFRESH": "Обновить плейлист",
"REFRESH_XTREAM": "Обновить Xtream плейлист с сервера",
"REFRESH_XTREAM_DIALOG": {
"TITLE": "Обновить Xtream плейлист",
"MESSAGE": "Это удалит локально кэшированные категории и потоки и повторно импортирует все с удаленного сервера. Ваши избранные и недавно просмотренные элементы будут сохранены. Вы уверены, что хотите продолжить?",
"STARTED": "Обновление начато. Переход к плейлисту...",
"ERROR": "Не удалось обновить плейлист. Пожалуйста, попробуйте снова."
},
"REMOVE_DIALOG": {
"MESSAGE": "Вы уверены, что хотите удалить этот плейлист?",
"TITLE": "Удалить плейлист",
"SUCCESS": "Плейлист был успешно удален"
},
"GLOBAL_FAVORITES": "Все фавориты",
"GLOBAL_FAVORITES_DESCRIPTION": "Автоматически сгенерированный плейлист, включающий в себя фавориты из всех плейлистов",
"MY_PLAYLISTS": "Мои плейлисты",
"MY_PLAYLISTS_SUBTITLE": "Все имеющиеся плейлисты",
"PLAYLIST_UPDATE_SUCCESS": "Плейлист был успешно обновлен!",
"PLAYLIST_UPDATE_ERROR": "Ошибка при обновлении деталей плейлиста.",
"AUTO_REFRESH_UPDATE_SUCCESS": "Успешно! Плейлисты были успешно обновлены",
"AUTO_REFRESH_ENABLED": "Автообновление включено",
"STALKER_PORTAL": "Stalker портал",
"XTREAM_PLAYLIST": "Xtream плейлист"
},
"FILE_UPLOAD": {
"DRAG_DROP": "Перетащите файл в эту область",
"OR": "или",
"CHOOSE_PLAYLIST": "выберите плейлист",
"REJECTED": "К сожалению, данный формат файлов не поддерживается {{filename}}."
},
"URL_UPLOAD": {
"PLAYLIST_URL": "Ссылка на плейлист (*.m3u или *.m3u8)",
"ADD_PLAYLIST": "Добавить плейлист",
"CORS_NOTE": "Примечание: Чтобы избежать проблем с CORS, приложение использует публичный CORS-прокси сервис. Если вы хотите загрузить плейлист с конфиденциальными данными, безопаснее импортировать его из файла."
},
"TEXT_IMPORT": {
"LABEL": "Вставьте плейлист формата m3u(8) в данное поле",
"BUTTON_LABEL": "Импортировать"
},
"XTREAM_PLAYLIST": {
"TITLE": "Название плейлиста",
"SERVER_URL": "Сервер URL",
"USERNAME": "Пользователь",
"PASSWORD": "Пароль",
"ADD": "Добавить",
"TEST_CREDENTIALS": "Проверить",
"URL_VALIDATION_ERROR": "Должен быть действительный URL-адрес с протоколом (например, http://example.com или https://example.com:4324)."
},
"STALKER_PORTAL": {
"ADD": "Добавить",
"TITLE": "Название",
"MAC_ADDRESS": "MAC-адрес",
"SERVER_URL": "URL сервера",
"URL_VALIDATION_ERROR": "Должен быть действительный URL-адрес с протоколом (например, http://example.com/c или https://example.com/stalker_portal/c)."
},
"FILTER_BY_TYPE": "Фильтровать по типу",
"FILTER_BY_NAME": "Фильтровать по имени",
"SORT_PLAYLISTS": "Сортировать плейлисты",
"SORT_BY": "Сортировать по",
"SORT_OPTIONS": {
"NAME_ASC": "Имени (А-Я)",
"NAME_DESC": "Имени (Я-А)",
"NEWEST": "Дате добавления (сначала новые)",
"OLDEST": "Дате добавления (сначала старые)"
},
"IMPORTED_AS_TEXT": "Импортировано как текст",
"PARSING_ERROR": "Ошибка парсинга, недействительный плейлист:"
},
"PLAYLISTS": {
"NO_PLAYLISTS": "Пока еще не было добавлено ни одного плейлиста",
"ADD_FIRST": "Вы можете добавить плейлисты из соседних вкладок",
"ADDED_VIA_URL": "Был добавлен по ссылке:",
"ADDED_VIA_FILE": "Был загружен с компьютера",
"CHANNELS": "Каналов",
"ADDED": "Добавлен",
"SHOW_DETAILS": "Показать информацию о плейлисте",
"REMOVE": "Удалить плейлист",
"SEARCH_PLAYLISTS": "Поиск по названию плейлиста",
"INFO_DIALOG": {
"PLAYLIST_DETAILS": "Подробная информация о плейлисте",
"TITLE": "Название",
"FROM_URL": "Добавлен по ссылке",
"IMPORT_DATE": "Дата импорта",
"CHANNELS": "Количество каналов",
"CLOSE": "Закрыть",
"SAVE": "Сохранить",
"USER_AGENT": "User agent",
"ORIGINAL_FILENAME": "Имя файла при загрузке",
"FILE_PATH": "Путь к файлу",
"UPDATE_FAILED": "Не удалось обновить плейлист",
"AUTO_UPDATE": "Автообновление",
"AUTO_UPDATE_DESCRIPTION": "Если активирована функция автообновления, список воспроизведения будет обновляться автоматически при каждом запуске приложения.",
"CUSTOM_USER_AGENT": "Некоторые IPTV провайдеры требуют указание определенного пользовательского агента (User Agent) для воспроизведения своего плейлиста.",
"EXPORT_PLAYLIST": "Экспортировать плейлист (m3u)",
"PASSWORD": "Пароль",
"SERVER_URL": "URL-адрес сервера",
"USERNAME": "Имя пользователя",
"MAC_ADDRESS": "MAC-адрес",
"PLAYLIST_EXPORT_SUCCESS": "Плейлист был успешно экспортирован.",
"PLAYLIST_EXPORT_ERROR": "Ошибка: не удалось экспортировать плейлист."
},
"UPDATED": "Обновлен",
"REFRESH": "Обновить плейлист",
"REFRESH_XTREAM": "Обновить Xtream плейлист с сервера",
"REFRESH_XTREAM_DIALOG": {
"TITLE": "Обновить Xtream плейлист",
"MESSAGE": "Это удалит локально кэшированные категории и потоки и повторно импортирует все с удаленного сервера. Ваши избранные и недавно просмотренные элементы будут сохранены. Вы уверены, что хотите продолжить?",
"STARTED": "Обновление начато. Переход к плейлисту...",
"ERROR": "Не удалось обновить плейлист. Пожалуйста, попробуйте снова."
},
"REMOVE_DIALOG": {
"MESSAGE": "Вы уверены, что хотите удалить этот плейлист?",
"TITLE": "Удалить плейлист",
"SUCCESS": "Плейлист был успешно удален"
},
"GLOBAL_FAVORITES": "Все фавориты",
"GLOBAL_FAVORITES_DESCRIPTION": "Автоматически сгенерированный плейлист, включающий в себя фавориты из всех плейлистов",
"MY_PLAYLISTS": "Мои плейлисты",
"MY_PLAYLISTS_SUBTITLE": "Все имеющиеся плейлисты",
"PLAYLIST_UPDATE_SUCCESS": "Плейлист был успешно обновлен!",
"PLAYLIST_UPDATE_ERROR": "Ошибка при обновлении деталей плейлиста.",
"AUTO_REFRESH_UPDATE_SUCCESS": "Успешно! Плейлисты были успешно обновлены",
"AUTO_REFRESH_ENABLED": "Автообновление включено",
"STALKER_PORTAL": "Stalker портал",
"XTREAM_PLAYLIST": "Xtream плейлист"
"SETTINGS": {
"TITLE": "Настройки / IPTVnator",
"SUBTITLE": "Здесь можно изменить общие настройки приложения",
"DESCRIPTION": "Закончив настройку, не забудьте подтвердить новые настройки кнопкой 'Сохранить'",
"EPG_URL_LABEL": "Ссылка на программу каналов (EPG) (*.xml или *.xml.gz)",
"EPG_URL_PLACEHOLDER": "Введите ссылку",
"REFRESH_EPG": "Обновить программу каналов (EPG)",
"VIDEO_PLAYER_LABEL": "Видеоплеер",
"VIDEO_PLAYER_PLACEHOLDER": "Выберите из списка",
"VERSION": "Версия приложения",
"SAVE_CHANGES": "Сохранить",
"BACK_TO_HOME": "Назад",
"NEW_VERSION_AVAILABLE": "Доступна новая версия приложения",
"LATEST_VERSION": "Вы используете последнюю версию приложения",
"STREAM_FORMAT": "Формат потока",
"LANGUAGE": "Язык",
"SETTINGS_SAVED": "Новые настройки были успешно сохранены!",
"THEME": "Тема оформления",
"SHOW_CAPTIONS": "Отображать субтитры",
"ADD_EPG_SOURCE": "Добавить источник EPG",
"EPG_URL_ERROR": "Введенное значение не является URL-адресом.",
"VIDEO_PLAYER_DESCRIPTION": "Выбрать стандартный видео плеер",
"STREAM_FORMAT_DESCRIPTION": "Выбрать формат потока по умолчанию (Xtream)",
"LANGUAGE_DESCRIPTION": "Выбрать язык приложения",
"THEME_DESCRIPTION": "Выбрать визуальную тему оформления",
"SHOW_CAPTIONS_DESCRIPTION": "Показать или скрыть субтитры по умолчанию.",
"VERSION_DESCRIPTION": "Текущая версия приложения",
"EPG_NOTE": "На данный момент EPG доступен только в нативной версии IPTVnator.",
"EPG_NOTE_URL_TEXT": "Подробнее здесь",
"IMPORT_EXPORT_DATA": "Импорт/экспорт плейлистов",
"IMPORT_EXPORT_DATA_DESCRIPTION": "Экспорт или импорт плейлистов в формате JSON, используемом в приложении.",
"IMPORT_DATA": "Импорт",
"EXPORT_DATA": "Экспорт",
"REMOVE_ALL": "Удалить плейлист",
"REMOVE_ALL_BUTTON": "Удалить",
"REMOVE_ALL_DESCRIPTION": " Это действие удалит все существующие плейлисты из приложения.",
"REMOVE_DIALOG": {
"TITLE": "Удалить все плейлисты",
"MESSAGE": "Вы уверены что хотите удалить все плейлисты из приложения?"
},
"IMPORT_ERROR": "Ошибка импорта, пожалуйста попробуйте импортировать другой файл.",
"PLAYLISTS_REMOVED": "Все плейлисты были удалены.",
"VLC_PLAYER_PATH_LABEL": "Путь к VLC плееру",
"VLC_PLAYER_PATH": "Путь к VLC плееру",
"VLC_PLAYER_PATH_DESCRIPTION": "Установите путь к проигрывателю VLC в вашей системе. \nОн будет использовать предоставленный путь к binary файлу VLC вместо пути по умолчанию.",
"MPV_PLAYER_PATH_LABEL": "Путь к mpv плееру",
"MPV_PLAYER_PATH": "Путь к mpv плееру",
"MPV_PLAYER_PATH_DESCRIPTION": "Установите путь к проигрывателю MPV в вашей системе. Он будет использовать предоставленный путь к бинарному файлу MPV вместо найденного в $PATH.",
"MPV_REUSE_INSTANCE_LABEL": "Переиспользовать экземпляр MPV",
"MPV_REUSE_INSTANCE_DESCRIPTION": "Когда включено, одно и то же окно MPV будет использоваться для воспроизведения разных потоков. Когда отключено, каждый поток открывается в новом окне MPV.",
"REMOTE_CONTROL": "Удалённое управление",
"REMOTE_CONTROL_DESCRIPTION": "Включить удалённое управление приложением.",
"REMOTE_CONTROL_PORT": "Порт удалённого управления",
"REMOTE_CONTROL_PORT_DESCRIPTION": "Порт для удалённого управления приложением. По умолчанию используется порт 8765.",
"PORT_REQUIRED": "Порт обязателен",
"PORT_MIN": "Порт должен быть не менее 1",
"PORT_MAX": "Порт должен быть не более 65535",
"PORT_PATTERN": "Разрешены только цифры"
},
"FILE_UPLOAD": {
"DRAG_DROP": "Перетащите файл в эту область",
"OR": "или",
"CHOOSE_PLAYLIST": "выберите плейлист",
"REJECTED": "К сожалению, данный формат файлов не поддерживается {{filename}}."
"THEMES": {
"DARK_THEME": "Тёмная тема",
"LIGHT_THEME": "Светлая тема"
},
"URL_UPLOAD": {
"PLAYLIST_URL": "Ссылка на плейлист (*.m3u или *.m3u8)",
"ADD_PLAYLIST": "Добавить плейлист",
"CORS_NOTE": "Примечание: Чтобы избежать проблем с CORS, приложение использует публичный CORS-прокси сервис. Если вы хотите загрузить плейлист с конфиденциальными данными, безопаснее импортировать его из файла."
"CHANNELS": {
"SEARCH_CHANNEL": "Поиск каналов",
"UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Загрузить или выбрать другой плейлист",
"ALL_CHANNELS": "Все каналы",
"GROUPS": "Группы",
"UNGROUPED": "Негруппированные",
"UNNAMED_CHANNEL": "Канал без названия",
"FAVORITES": "Избранные",
"NO_FAVORITES": "Избранных каналов пока нет",
"USE_STAR_TO_FAVORITE": "Используйте иконку звезды, чтобы добавлять каналы в избранное",
"REMOVE_FAVORITE": "Удалить из списка фаворитов",
"FAVORITES_UPDATED": "Список фаворитов был обновлен!",
"SORT_BY": "Сортировать по",
"SORT_BY_DATE": "Сортировать по дате добавления (сначала новые)",
"SORT_BY_RATING": "Сортировать по рейтингу",
"SORT_BY_NAME": "Сортировать по имени (А-Я)"
},
"TEXT_IMPORT": {
"LABEL": "Вставьте плейлист формата m3u(8) в данное поле",
"BUTTON_LABEL": "Импортировать"
"TOP_MENU": {
"OPEN_CHANNELS_LIST": "Открыть список каналов",
"TOGGLE_FAVORITE_FLAG": "Избранный",
"OPEN_EPG_LIST": "Открыть программу (EPG)",
"OPEN_MULTI_EPG": "Открыть Multi-EPG вид"
},
"XTREAM_PLAYLIST": {
"TITLE": "Название плейлиста",
"SERVER_URL": "Сервер URL",
"USERNAME": "Пользователь",
"PASSWORD": "Пароль",
"ADD": "Добавить",
"TEST_CREDENTIALS": "Проверить",
"URL_VALIDATION_ERROR": "Должен быть действительный URL-адрес с протоколом (например, http://example.com или https://example.com:4324)."
"EPG": {
"TITLE": "Электронный программный гид",
"NEXT_DAY": "Следующий день",
"PREVIOUS_DAY": "Предыдущий день",
"LIVE_NOW": "Сейчас",
"LIVE_STREAM": "Прямая трансляция",
"TIMESHIFT_AVAILABLE": "Доступна функция архива (timeshift). Нажмите на передачу для воспроизведения.",
"EPG_NOT_AVAILABLE_DATE": "Ой, программа на этот день, к сожалению, отсутствует.",
"EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Программа для этого канала недоступна.",
"EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Попробуйте изменить или обновить ссылку на EPG в настройках приложения.",
"FETCH_EPG": "Загружаю программу ТВ-передач (EPG)...",
"ERROR": "Ой, не удалось загрузить программу каналов (EPG)",
"DOWNLOAD_SUCCESS": "Программа каналов (EPG) была успешно загружена.",
"FETCH_SUCCESS": "Программа каналов (EPG) была успешно получена",
"PROGRAM_DIALOG": {
"PROGRAM_DETAILS": "Подробности передачи",
"TITLE": "Название",
"CATEGORY": "Категория",
"PARENTAL_RATING_SYSTEM": "Возрастное ограничение",
"DESCRIPTION": "Описание",
"LANGUAGE": "Язык",
"SHOW_PROGRAM_DETAILS": "Показать подробности"
}
},
"STALKER_PORTAL": {
"ADD": "Добавить",
"TITLE": "Название",
"MAC_ADDRESS": "MAC-адрес",
"SERVER_URL": "URL сервера",
"URL_VALIDATION_ERROR": "Должен быть действительный URL-адрес с протоколом (например, http://example.com/c или https://example.com/stalker_portal/c)."
"LANGUAGES": {
"ARABIC": "العربية",
"MOROCCAN_ARABIC": "العربية (اللهجة المغربية)",
"ENGLISH": "English",
"KOREAN": "한국어",
"RUSSIAN": "Русский",
"GERMAN": "Deutsch",
"SPANISH": "Español",
"CHINESE": "简体中文",
"TRADITIONAL_CHINESE": "繁體中文",
"FRENCH": "Français",
"ITALIAN": "Italiano",
"TURKISH": "Türkçe",
"JAPANESE": "日本語",
"DUTCH": "Nederlands",
"BELARUSIAN": "Беларуская",
"POLISH": "Polski",
"PORTUGUESE": "Português"
},
"FILTER_BY_TYPE": "Фильтровать по типу",
"FILTER_BY_NAME": "Фильтровать по имени",
"SORT_PLAYLISTS": "Сортировать плейлисты",
"SORT_BY": "Сортировать по",
"SORT_OPTIONS": {
"NAME_ASC": "Имени (А-Я)",
"NAME_DESC": "Имени (Я-А)",
"NEWEST": "Дате добавления (сначала новые)",
"OLDEST": "Дате добавления (сначала старые)"
"NO": "Нет",
"YES": "Да",
"MENU": {
"OPEN": "Открыть меню",
"SUPPORT_ARIA": "Поддержать IPTVnator на GitHub",
"SETTINGS": "Настройки",
"SETTINGS_ARIA": "Открыть настройки",
"BUG_REPORT": "Сообщить об ошибке",
"WHAT_IS_NEW": "Что нового?",
"ABOUT": "О приложении",
"ABOUT_ARIA": "О приложении",
"SUPPORT_PROJECT": "Поддержать проект"
},
"IMPORTED_AS_TEXT": "Импортировано как текст",
"PARSING_ERROR": "Ошибка парсинга, недействительный плейлист:"
},
"SETTINGS": {
"TITLE": "Настройки / IPTVnator",
"SUBTITLE": "Здесь можно изменить общие настройки приложения",
"DESCRIPTION": "Закончив настройку, не забудьте подтвердить новые настройки кнопкой 'Сохранить'",
"EPG_URL_LABEL": "Ссылка на программу каналов (EPG) (*.xml или *.xml.gz)",
"EPG_URL_PLACEHOLDER": "Введите ссылку",
"REFRESH_EPG": "Обновить программу каналов (EPG)",
"VIDEO_PLAYER_LABEL": "Видеоплеер",
"VIDEO_PLAYER_PLACEHOLDER": "Выберите из списка",
"VERSION": "Версия приложения",
"SAVE_CHANGES": "Сохранить",
"BACK_TO_HOME": "Назад",
"NEW_VERSION_AVAILABLE": "Доступна новая версия приложения",
"LATEST_VERSION": "Вы используете последнюю версию приложения",
"STREAM_FORMAT": "Формат потока",
"LANGUAGE": "Язык",
"SETTINGS_SAVED": "Новые настройки были успешно сохранены!",
"THEME": "Тема оформления",
"SHOW_CAPTIONS": "Отображать субтитры",
"ADD_EPG_SOURCE": "Добавить источник EPG",
"EPG_URL_ERROR": "Введенное значение не является URL-адресом.",
"VIDEO_PLAYER_DESCRIPTION": "Выбрать стандартный видео плеер",
"STREAM_FORMAT_DESCRIPTION": "Выбрать формат потока по умолчанию (Xtream)",
"LANGUAGE_DESCRIPTION": "Выбрать язык приложения",
"THEME_DESCRIPTION": "Выбрать визуальную тему оформления",
"SHOW_CAPTIONS_DESCRIPTION": "Показать или скрыть субтитры по умолчанию.",
"VERSION_DESCRIPTION": "Текущая версия приложения",
"EPG_NOTE": "На данный момент EPG доступен только в нативной версии IPTVnator.",
"EPG_NOTE_URL_TEXT": "Подробнее здесь",
"IMPORT_EXPORT_DATA": "Импорт/экспорт плейлистов",
"IMPORT_EXPORT_DATA_DESCRIPTION": "Экспорт или импорт плейлистов в формате JSON, используемом в приложении.",
"IMPORT_DATA": "Импорт",
"EXPORT_DATA": "Экспорт",
"REMOVE_ALL": "Удалить плейлист",
"REMOVE_ALL_BUTTON": "Удалить",
"REMOVE_ALL_DESCRIPTION": " Это действие удалит все существующие плейлисты из приложения.",
"REMOVE_DIALOG": {
"TITLE": "Удалить все плейлисты",
"MESSAGE": "Вы уверены что хотите удалить все плейлисты из приложения?"
"ABOUT": {
"TITLE": "О приложении",
"DESCRIPTION": "Кроссплатформенное приложение IPTV-плеера с множеством функций, таких как поддержка плейлистов m3u и m3u8, избранное, телегид, ТВ-архив/catchup и многое другое.",
"VERSION": "Версия",
"GITHUB_TOOLTIP": "Репозиторий приложения на GitHub",
"BUY_ME_A_COFFEE_TOOLTIP": "Поддержать разработчика",
"TWITTER_TOOLTIP": "Поделиться на Twitter"
},
"IMPORT_ERROR": "Ошибка импорта, пожалуйста попробуйте импортировать другой файл.",
"PLAYLISTS_REMOVED": "Все плейлисты были удалены.",
"VLC_PLAYER_PATH_LABEL": "Путь к VLC плееру",
"VLC_PLAYER_PATH": "Путь к VLC плееру",
"VLC_PLAYER_PATH_DESCRIPTION": "Установите путь к проигрывателю VLC в вашей системе. \nОн будет использовать предоставленный путь к binary файлу VLC вместо пути по умолчанию.",
"MPV_PLAYER_PATH_LABEL": "Путь к mpv плееру",
"MPV_PLAYER_PATH": "Путь к mpv плееру",
"MPV_PLAYER_PATH_DESCRIPTION": "Установите путь к проигрывателю MPV в вашей системе. Он будет использовать предоставленный путь к бинарному файлу MPV вместо найденного в $PATH.",
"MPV_REUSE_INSTANCE_LABEL": "Переиспользовать экземпляр MPV",
"MPV_REUSE_INSTANCE_DESCRIPTION": "Когда включено, одно и то же окно MPV будет использоваться для воспроизведения разных потоков. Когда отключено, каждый поток открывается в новом окне MPV.",
"REMOTE_CONTROL": "Дистанционное управление",
"REMOTE_CONTROL_DESCRIPTION": "Включить дистанционное управление приложением.",
"REMOTE_CONTROL_PORT": "Порт дистанционного управления",
"REMOTE_CONTROL_PORT_DESCRIPTION": "Порт, используемый для дистанционного управления приложением. По умолчанию используется порт 3000."
},
"THEMES": {
"DARK_THEME": "Тёмная тема",
"LIGHT_THEME": "Светлая тема"
},
"CHANNELS": {
"SEARCH_CHANNEL": "Поиск каналов",
"UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Загрузить или выбрать другой плейлист",
"ALL_CHANNELS": "Все каналы",
"GROUPS": "Группы",
"UNGROUPED": "Негруппированные",
"UNNAMED_CHANNEL": "Канал без названия",
"FAVORITES": "Избранные",
"NO_FAVORITES": "Избранных каналов пока нет",
"USE_STAR_TO_FAVORITE": "Используйте иконку звезды, чтобы добавлять каналы в избранное",
"REMOVE_FAVORITE": "Удалить из списка фаворитов",
"FAVORITES_UPDATED": "Список фаворитов был обновлен!",
"SORT_BY": "Сортировать по",
"SORT_BY_DATE": "Сортировать по дате добавления (сначала новые)",
"SORT_BY_RATING": "Сортировать по рейтингу",
"SORT_BY_NAME": "Сортировать по имени (А-Я)"
},
"TOP_MENU": {
"OPEN_CHANNELS_LIST": "Открыть список каналов",
"TOGGLE_FAVORITE_FLAG": "Избранный",
"OPEN_EPG_LIST": "Открыть программу (EPG)",
"OPEN_MULTI_EPG": "Открыть Multi-EPG вид"
},
"EPG": {
"TITLE": "Электронный программный гид",
"NEXT_DAY": "Следующий день",
"PREVIOUS_DAY": "Предыдущий день",
"LIVE_NOW": "Сейчас",
"LIVE_STREAM": "Прямая трансляция",
"TIMESHIFT_AVAILABLE": "Доступна функция архива (timeshift). Нажмите на передачу для воспроизведения.",
"EPG_NOT_AVAILABLE_DATE": "Ой, программа на этот день, к сожалению, отсутствует.",
"EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Программа для этого канала недоступна.",
"EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Попробуйте изменить или обновить ссылку на EPG в настройках приложения.",
"FETCH_EPG": "Загружаю программу ТВ-передач (EPG)...",
"ERROR": "Ой, не удалось загрузить программу каналов (EPG)",
"DOWNLOAD_SUCCESS": "Программа каналов (EPG) была успешно загружена.",
"FETCH_SUCCESS": "Программа каналов (EPG) была успешно получена",
"PROGRAM_DIALOG": {
"PROGRAM_DETAILS": "Подробности передачи",
"TITLE": "Название",
"CATEGORY": "Категория",
"PARENTAL_RATING_SYSTEM": "Возрастное ограничение",
"DESCRIPTION": "Описание",
"LANGUAGE": "Язык",
"SHOW_PROGRAM_DETAILS": "Показать подробности"
"UPDATE_AVAILABLE": "Доступно обновление",
"REFRESH": "Обновить",
"XTREAM": {
"RATING": "Рейтинг",
"RELEASE_DATE": "Дата выпуска",
"ACTORS": "Актеры",
"PLAY": "Воспроизвести",
"OPENING_PLAYER": "Открытие плеера...",
"GENRE": "Жанр",
"DURATION": "Продолжительность",
"COUNTRY": "Страна",
"NAME": "Название",
"DIRECTOR": "Режиссёр",
"CAST": "Бросать",
"EPISODE_RUN_TIME": "Длительность эпизода",
"AGE": "Возраст",
"EPISODE": "Эпизод",
"YEAR": "Год",
"YOUTUBE_TRAILER": "YouTube-трейлер",
"DESCRIPTION": "Описание",
"KINOPOISK_RATING": "Рейтинг на Кинопоиск",
"IMDB_RATING": "Рейтинг на IMDB",
"ACCOUNT_INFO": {
"TITLE": "Информация об аккаунте",
"USER_INFO": "Информация о пользователе",
"STATUS": "Статус",
"USERNAME": "Имя пользователя",
"ACTIVE_CONNECTIONS": "Активные подключения",
"CREATED": "Создано",
"EXPIRES": "Истекает",
"TRIAL_ACCOUNT": "Пробный аккаунт",
"YES": "Да",
"NO": "Нет",
"ALLOWED_FORMATS": "Разрешенные форматы",
"SERVER_INFO": "Информация о сервере",
"LIVE_TV": "Прямой эфир",
"MOVIES": "Фильмы",
"TV_SERIES": "Сериалы",
"URL": "URL",
"PROTOCOL": "Протокол",
"TIMEZONE": "Часовой пояс",
"SERVER_TIME": "Время сервера",
"PORTS": "Порты",
"HTTP_PORT": "HTTP",
"HTTPS_PORT": "HTTPS",
"RTMP_PORT": "RTMP"
}
},
"PORTALS": {
"ALL_CATEGORIES": "Все категории",
"ADD_TO_FAVORITES": "Добавить в избранное",
"REMOVE_FROM_FAVORITES": "Удалить из избранного",
"MY_FAVORITES": "Избранное",
"COPY_STREAM_URL": "Копировать URL-адрес потока",
"ERROR_VIEW": {
"DELETE": "Удалить",
"ACCOUNT_EXPIRED": {
"TITLE": "Срок действия учетной записи истек",
"DESCRIPTION": "Срок действия этой учетной записи истек. \nПожалуйста, проверьте учетные данные в настройках плейлиста или удалите его."
},
"HOME": "На главную",
"EMPTY_CATEGORY": {
"TITLE": "Пустая категория",
"DESCRIPTION": "В этой категории нет контента. Выберите другую категорию или измените тип контента."
},
"UNAUTHORIZED": {
"TITLE": "Несанкционированный",
"DESCRIPTION": "Пожалуйста, проверьте учетные данные в настройках плейлиста или удалите их."
},
"NOT_FOUND": {
"TITLE": "Портал не найден",
"DESCRIPTION": "Пожалуйста, проверьте учетные данные и URL портала в настройках плейлиста или удалите его."
},
"NO_CATEGORY_SELECTED": {
"TITLE": "Категория не выбрана",
"DESCRIPTION": "Пожалуйста, выберите категорию для просмотра содержимого"
},
"UNKNOWN_ERROR": {
"TITLE": "Что-то пошло не так",
"DESCRIPTION": "Неизвестная ошибка. Повторите попытку позже."
},
"PLAYLIST_SETTINGS": "Настройки плейлиста"
},
"STREAM_URL_COPIED": "URL-адрес потока был скопирован в буфер обмена.",
"EMPTY_LIST_VIEW": {
"TITLE": "Результаты не найдены",
"NO_SEARCH_RESULTS": "Ничего не найдено, попробуйте изменить поисковый запрос"
},
"SIDEBAR": {
"MAIN": "Главная",
"MOVIES": "Фильмы",
"LIVE_TV": "Прямой эфир",
"SERIES": "Сериалы",
"LIBRARY": "Библиотека",
"SEARCH": "Поиск",
"RECENT": "Недавние",
"FAVORITES": "Избранное",
"HOME": "Домой",
"PLAYLIST_INFO": "Информация о плейлисте",
"ACCOUNT_INFO": "Информация об аккаунте"
},
"REMOVED_FROM_FAVORITES": "Удалено из избранного",
"ADDED_TO_FAVORITES": "Добавлено в избранное",
"SELECT_CATEGORY": "Выберите категорию",
"PAGE": "Страница"
},
"INFORMATION": "Инфо",
"REMOTE_CONTROL": {
"TITLE": "Дистанционное управление",
"UP_CHANNEL": "Канал вверх",
"DOWN_CHANNEL": "Канал вниз",
"HEADER": "IPTVNator Дистанционное управление",
"FOOTER": "IPTVNator Дистанционное управление"
}
},
"LANGUAGES": {
"ARABIC": "العربية",
"MOROCCAN_ARABIC": "العربية (اللهجة المغربية)",
"ENGLISH": "English",
"KOREAN": "한국어",
"RUSSIAN": "Русский",
"GERMAN": "Deutsch",
"SPANISH": "Español",
"CHINESE": "简体中文",
"TRADITIONAL_CHINESE": "繁體中文",
"FRENCH": "Français",
"ITALIAN": "Italiano",
"TURKISH": "Türkçe",
"JAPANESE": "日本語",
"DUTCH": "Nederlands",
"BELARUSIAN": "Беларуская",
"POLISH": "Polski",
"PORTUGUESE": "Português"
},
"NO": "Нет",
"YES": "Да",
"MENU": {
"OPEN": "Открыть меню",
"SUPPORT_ARIA": "Поддержать IPTVnator на GitHub",
"SETTINGS": "Настройки",
"SETTINGS_ARIA": "Открыть настройки",
"BUG_REPORT": "Сообщить об ошибке",
"WHAT_IS_NEW": "Что нового?",
"ABOUT": "О приложении",
"ABOUT_ARIA": "О приложении",
"SUPPORT_PROJECT": "Поддержать проект"
},
"ABOUT": {
"TITLE": "О приложении",
"DESCRIPTION": "Кроссплатформенное приложение IPTV-плеера с множеством функций, таких как поддержка плейлистов m3u и m3u8, избранное, телегид, ТВ-архив/catchup и многое другое.",
"VERSION": "Версия",
"GITHUB_TOOLTIP": "Репозиторий приложения на GitHub",
"BUY_ME_A_COFFEE_TOOLTIP": "Поддержать разработчика",
"TWITTER_TOOLTIP": "Поделиться на Twitter"
},
"UPDATE_AVAILABLE": "Доступно обновление",
"REFRESH": "Обновить",
"XTREAM": {
"RATING": "Рейтинг",
"RELEASE_DATE": "Дата выпуска",
"ACTORS": "Актеры",
"PLAY": "Воспроизвести",
"OPENING_PLAYER": "Открытие плеера...",
"GENRE": "Жанр",
"DURATION": "Продолжительность",
"COUNTRY": "Страна",
"NAME": "Название",
"DIRECTOR": "Режиссёр",
"CAST": "Бросать",
"EPISODE_RUN_TIME": "Длительность эпизода",
"AGE": "Возраст",
"EPISODE": "Эпизод",
"YEAR": "Год",
"YOUTUBE_TRAILER": "YouTube-трейлер",
"DESCRIPTION": "Описание",
"KINOPOISK_RATING": "Рейтинг на Кинопоиск",
"IMDB_RATING": "Рейтинг на IMDB",
"ACCOUNT_INFO": {
"TITLE": "Информация об аккаунте",
"USER_INFO": "Информация о пользователе",
"STATUS": "Статус",
"USERNAME": "Имя пользователя",
"ACTIVE_CONNECTIONS": "Активные подключения",
"CREATED": "Создано",
"EXPIRES": "Истекает",
"TRIAL_ACCOUNT": "Пробный аккаунт",
"YES": "Да",
"NO": "Нет",
"ALLOWED_FORMATS": "Разрешенные форматы",
"SERVER_INFO": "Информация о сервере",
"LIVE_TV": "Прямой эфир",
"MOVIES": "Фильмы",
"TV_SERIES": "Сериалы",
"URL": "URL",
"PROTOCOL": "Протокол",
"TIMEZONE": "Часовой пояс",
"SERVER_TIME": "Время сервера",
"PORTS": "Порты",
"HTTP_PORT": "HTTP",
"HTTPS_PORT": "HTTPS",
"RTMP_PORT": "RTMP"
}
},
"PORTALS": {
"ALL_CATEGORIES": "Все категории",
"ADD_TO_FAVORITES": "Добавить в избранное",
"REMOVE_FROM_FAVORITES": "Удалить из избранного",
"MY_FAVORITES": "Избранное",
"COPY_STREAM_URL": "Копировать URL-адрес потока",
"ERROR_VIEW": {
"DELETE": "Удалить",
"ACCOUNT_EXPIRED": {
"TITLE": "Срок действия учетной записи истек",
"DESCRIPTION": "Срок действия этой учетной записи истек. \nПожалуйста, проверьте учетные данные в настройках плейлиста или удалите его."
},
"HOME": "На главную",
"EMPTY_CATEGORY": {
"TITLE": "Пустая категория",
"DESCRIPTION": "В этой категории нет контента. Выберите другую категорию или измените тип контента."
},
"UNAUTHORIZED": {
"TITLE": "Несанкционированный",
"DESCRIPTION": "Пожалуйста, проверьте учетные данные в настройках плейлиста или удалите их."
},
"NOT_FOUND": {
"TITLE": "Портал не найден",
"DESCRIPTION": "Пожалуйста, проверьте учетные данные и URL портала в настройках плейлиста или удалите его."
},
"NO_CATEGORY_SELECTED": {
"TITLE": "Категория не выбрана",
"DESCRIPTION": "Пожалуйста, выберите категорию для просмотра содержимого"
},
"UNKNOWN_ERROR": {
"TITLE": "Что-то пошло не так",
"DESCRIPTION": "Неизвестная ошибка. Повторите попытку позже."
},
"PLAYLIST_SETTINGS": "Настройки плейлиста"
},
"STREAM_URL_COPIED": "URL-адрес потока был скопирован в буфер обмена.",
"EMPTY_LIST_VIEW": {
"TITLE": "Результаты не найдены",
"NO_SEARCH_RESULTS": "Ничего не найдено, попробуйте изменить поисковый запрос"
},
"SIDEBAR": {
"MAIN": "Главная",
"MOVIES": "Фильмы",
"LIVE_TV": "Прямой эфир",
"SERIES": "Сериалы",
"LIBRARY": "Библиотека",
"SEARCH": "Поиск",
"RECENT": "Недавние",
"FAVORITES": "Избранное",
"HOME": "Домой",
"PLAYLIST_INFO": "Информация о плейлисте",
"ACCOUNT_INFO": "Информация об аккаунте"
},
"REMOVED_FROM_FAVORITES": "Удалено из избранного",
"ADDED_TO_FAVORITES": "Добавлено в избранное",
"SELECT_CATEGORY": "Выберите категорию",
"PAGE": "Страница"
},
"INFORMATION": "Инфо",
"REMOTE_CONTROL": {
"TITLE": "Дистанционное управление",
"UP_CHANNEL": "Канал вверх",
"DOWN_CHANNEL": "Канал вниз",
"HEADER": "IPTVNator Дистанционное управление",
"FOOTER": "IPTVNator Дистанционное управление"
}
}
}
Vendored
+4
View File
@@ -156,6 +156,10 @@ declare global {
xtreamId: number,
playlistId: string
) => Promise<any | null>;
// Remote control
onChannelChange?: (
callback: (data: { direction: 'up' | 'down' }) => void
) => void;
};
process: NodeJS.Process;
require: NodeRequire;
+1
View File
@@ -9,6 +9,7 @@ export * from './lib/epg-list/epg-list.component';
export * from './lib/html-video-player/html-video-player.component';
export * from './lib/info-overlay/info-overlay.component';
export * from './lib/multi-epg/multi-epg-container.component';
export * from './lib/multi-epg/overlay-ref.token';
export * from './lib/recent-playlists/playlist-info/playlist-info.component';
export * from './lib/recent-playlists/playlist-item/playlist-item.component';
export * from './lib/recent-playlists/recent-playlists.component';
@@ -42,7 +42,7 @@
"
[logo]="channel?.tvg?.logo"
(clicked)="selectChannel(channel)"
[selected]="selected?.id === channel?.id"
[selected]="activeChannel()?.url === channel?.url"
/>
</cdk-virtual-scroll-viewport>
</mat-nav-list>
@@ -90,7 +90,7 @@
[logo]="channel?.tvg?.logo"
(clicked)="selectChannel(channel)"
[selected]="
selected?.id === channel.id
activeChannel()?.url === channel.url
"
></app-channel-list-item>
}
@@ -136,7 +136,7 @@
[isDraggable]="true"
[logo]="channel?.tvg?.logo"
(clicked)="selectChannel(channel)"
[selected]="selected?.id === channel?.id"
[selected]="activeChannel()?.url === channel?.url"
[showFavoriteButton]="true"
(favoriteToggled)="
toggleFavoriteChannel(channel, $event)
@@ -28,7 +28,11 @@ import { Store } from '@ngrx/store';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import * as _ from 'lodash';
import * as PlaylistActions from 'm3u-state';
import { selectActivePlaylistId, selectFavorites } from 'm3u-state';
import {
selectActive,
selectActivePlaylistId,
selectFavorites,
} from 'm3u-state';
import { map, skipWhile } from 'rxjs';
import { EpgService } from 'services';
import { Channel } from 'shared-interfaces';
@@ -81,7 +85,7 @@ export class ChannelListContainerComponent implements OnDestroy {
groupedChannels!: { [key: string]: Channel[] };
/** Selected channel */
selected!: Channel;
readonly activeChannel = this.store.selectSignal(selectActive);
/** Search term for channel filter */
searchTerm: { name: string } = {
@@ -123,11 +127,10 @@ export class ChannelListContainerComponent implements OnDestroy {
);
/**
* Sets clicked channel as selected and emits them to the parent component
* Sets clicked channel as active and dispatches to store
* @param channel selected channel
*/
selectChannel(channel: Channel): void {
this.selected = channel;
this.store.dispatch(PlaylistActions.setActiveChannel({ channel }));
// Use tvg-id for EPG matching, fallback to channel name if not available
@@ -7,7 +7,6 @@ import {
Component,
ElementRef,
inject,
InjectionToken,
Input,
OnDestroy,
OnInit,
@@ -23,6 +22,7 @@ import { addDays, differenceInMinutes, format, parse, subDays } from 'date-fns';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { Channel, EpgChannel, EpgProgram } from 'shared-interfaces';
import { EpgItemDescriptionComponent } from '../epg-list/epg-item-description/epg-item-description.component';
import { COMPONENT_OVERLAY_REF } from './overlay-ref.token';
interface EnrichedProgram extends EpgProgram {
startDate: Date;
@@ -89,13 +89,7 @@ export class MultiEpgContainerComponent
private readonly dialog = inject(MatDialog);
private readonly cdr = inject(ChangeDetectorRef);
private readonly COMPONENT_OVERLAY_REF = new InjectionToken<OverlayRef>(
'COMPONENT_OVERLAY_REF'
);
private readonly overlayRef = inject<OverlayRef>(
this.COMPONENT_OVERLAY_REF
);
private readonly overlayRef = inject<OverlayRef>(COMPONENT_OVERLAY_REF);
ngOnInit() {
this.calculateCurrentTimeBar();
@@ -0,0 +1,9 @@
import { OverlayRef } from '@angular/cdk/overlay';
import { InjectionToken } from '@angular/core';
/**
* Injection token for providing OverlayRef to dynamically created overlay components
*/
export const COMPONENT_OVERLAY_REF = new InjectionToken<OverlayRef>(
'COMPONENT_OVERLAY_REF'
);
+7
View File
@@ -0,0 +1,7 @@
# remote-control
This library was generated with [Nx](https://nx.dev).
## Running unit tests
Run `nx test remote-control` to execute the unit tests.
+34
View File
@@ -0,0 +1,34 @@
import nx from '@nx/eslint-plugin';
import baseConfig from '../../../eslint.config.mjs';
export default [
...baseConfig,
...nx.configs['flat/angular'],
...nx.configs['flat/angular-template'],
{
files: ['**/*.ts'],
rules: {
'@angular-eslint/directive-selector': [
'error',
{
type: 'attribute',
prefix: 'lib',
style: 'camelCase',
},
],
'@angular-eslint/component-selector': [
'error',
{
type: 'element',
prefix: 'lib',
style: 'kebab-case',
},
],
},
},
{
files: ['**/*.html'],
// Override or add rules here
rules: {},
},
];
+21
View File
@@ -0,0 +1,21 @@
export default {
displayName: 'remote-control',
preset: '../../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../../coverage/libs/ui/remote-control',
transform: {
'^.+\\.(ts|mjs|js|html)$': [
'jest-preset-angular',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$',
},
],
},
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes',
'jest-preset-angular/build/serializers/ng-snapshot',
'jest-preset-angular/build/serializers/html-comment',
],
};
+21
View File
@@ -0,0 +1,21 @@
{
"name": "remote-control",
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/ui/remote-control/src",
"prefix": "lib",
"projectType": "library",
"tags": [],
"targets": {
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
"options": {
"jestConfig": "libs/ui/remote-control/jest.config.ts",
"tsConfig": "libs/ui/remote-control/tsconfig.spec.json"
}
},
"lint": {
"executor": "@nx/eslint:lint"
}
}
}
+2
View File
@@ -0,0 +1,2 @@
export * from './lib/remote-control/remote-control.component';
export * from './lib/remote-control/remote-control.service';
@@ -0,0 +1,77 @@
<div class="remote">
<div class="remote__body">
<!-- App branding -->
<div class="remote__header">
<div class="remote__logo">IPTVnator</div>
</div>
<!-- Error state -->
@if (error) {
<div class="remote__error">
{{ error }}
</div>
}
<!-- Main touchpad circle -->
<div class="touchpad">
<div class="touchpad__glass">
<!-- Up button (top half) -->
<button
class="touchpad__button touchpad__button--up"
(click)="changeChannelUp()"
[disabled]="isLoading"
[class.is-loading]="isLoading"
aria-label="Channel Up"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
>
<path d="M5 15l7-7 7 7" />
</svg>
</button>
<!-- Down button (bottom half) -->
<button
class="touchpad__button touchpad__button--down"
(click)="changeChannelDown()"
[disabled]="isLoading"
[class.is-loading]="isLoading"
aria-label="Channel Down"
>
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
>
<path d="M19 9l-7 7-7-7" />
</svg>
</button>
<!-- Center indicator -->
<div class="touchpad__center">
<div
class="touchpad__pulse"
[class.is-active]="!isLoading"
></div>
</div>
</div>
<!-- Subtle edge highlights -->
<div class="touchpad__edge"></div>
</div>
<!-- Status indicator -->
<div class="remote__status">
<div class="status-dot" [class.is-active]="!isLoading"></div>
<span class="status-text">{{
isLoading ? 'Switching' : 'Ready'
}}</span>
</div>
</div>
</div>
@@ -0,0 +1,398 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
user-select: none;
}
// Main container
.remote {
min-height: 100vh;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #1a1a1a;
font-family:
-apple-system, BlinkMacSystemFont, 'SF Pro Display', 'Helvetica Neue',
sans-serif;
padding: 1rem;
background-image:
radial-gradient(
circle at 50% 20%,
rgba(10, 132, 255, 0.15) 0%,
transparent 50%
),
radial-gradient(
circle at 80% 80%,
rgba(120, 120, 255, 0.08) 0%,
transparent 50%
);
}
.remote__body {
width: 100%;
max-width: 420px;
display: flex;
flex-direction: column;
gap: 2.5rem;
animation: fadeInUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) backwards;
}
// Header
.remote__header {
text-align: center;
}
.remote__logo {
font-size: 1.5rem;
font-weight: 600;
letter-spacing: -0.02em;
color: #ffffff;
opacity: 0;
animation: fadeIn 1s ease-out 0.3s forwards;
}
// Error message
.remote__error {
background: rgba(255, 69, 58, 0.15);
border: 1px solid rgba(255, 69, 58, 0.4);
border-radius: 1rem;
padding: 1rem 1.25rem;
color: #ffb3ab;
font-size: 0.875rem;
font-weight: 500;
text-align: center;
animation: slideDown 0.4s cubic-bezier(0.16, 1, 0.3, 1);
}
// Touchpad - Main control
.touchpad {
position: relative;
width: 100%;
aspect-ratio: 1;
max-width: 360px;
margin: 0 auto;
animation: scaleIn 0.9s cubic-bezier(0.16, 1, 0.3, 1) 0.2s backwards;
}
.touchpad__glass {
position: relative;
width: 100%;
height: 100%;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
backdrop-filter: blur(20px);
box-shadow:
0 16px 48px rgba(0, 0, 0, 0.6),
0 0 0 1px rgba(255, 255, 255, 0.15),
inset 0 1px 1px rgba(255, 255, 255, 0.1),
inset 0 -1px 1px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.3);
overflow: hidden;
// Top reflection
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 45%;
background: linear-gradient(
180deg,
rgba(255, 255, 255, 0.25) 0%,
rgba(255, 255, 255, 0.1) 50%,
transparent 100%
);
border-radius: 50% 50% 0 0;
pointer-events: none;
}
// Bottom shadow
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 35%;
background: linear-gradient(
0deg,
rgba(0, 0, 0, 0.4) 0%,
rgba(0, 0, 0, 0.1) 50%,
transparent 100%
);
border-radius: 0 0 50% 50%;
pointer-events: none;
}
}
.touchpad__edge {
position: absolute;
inset: -2px;
border-radius: 50%;
background: radial-gradient(
circle at 50% 0%,
rgba(255, 255, 255, 0.15),
transparent 50%
);
opacity: 0.6;
pointer-events: none;
filter: blur(1px);
}
// Touch buttons
.touchpad__button {
position: absolute;
left: 0;
width: 100%;
height: 50%;
background: transparent;
border: none;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
transition: all 0.3s cubic-bezier(0.16, 1, 0.3, 1);
svg {
width: 56px;
height: 56px;
color: #ffffff;
stroke: #ffffff;
opacity: 0.5;
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
filter: drop-shadow(0 2px 8px rgba(0, 0, 0, 0.3));
}
&:hover:not(:disabled) svg {
opacity: 0.9;
transform: scale(1.05);
}
&:active:not(:disabled) {
svg {
opacity: 1;
transform: scale(0.92);
}
&::after {
opacity: 1;
}
}
// Ripple effect
&::after {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(
circle at center,
rgba(10, 132, 255, 0.5) 0%,
transparent 70%
);
opacity: 0;
transition: opacity 0.2s ease;
}
&:disabled {
cursor: not-allowed;
opacity: 0.4;
}
&.is-loading svg {
opacity: 0.5;
animation: pulse 1.5s ease-in-out infinite;
}
}
.touchpad__button--up {
top: 0;
border-radius: 50% 50% 0 0;
svg {
margin-bottom: 20%;
}
}
.touchpad__button--down {
bottom: 0;
border-radius: 0 0 50% 50%;
svg {
margin-top: 20%;
}
}
// Center indicator
.touchpad__center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 48px;
height: 48px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.08);
display: flex;
align-items: center;
justify-content: center;
pointer-events: none;
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3);
}
.touchpad__pulse {
width: 16px;
height: 16px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.2);
transition: all 0.4s ease;
&.is-active {
background: #0a84ff;
box-shadow:
0 0 12px rgba(10, 132, 255, 0.5),
0 0 24px rgba(10, 132, 255, 0.5);
animation: breathe 3s ease-in-out infinite;
}
}
// Status bar
.remote__status {
display: flex;
align-items: center;
justify-content: center;
gap: 0.625rem;
padding: 0.875rem 1.25rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 1.25rem;
border: 1px solid rgba(255, 255, 255, 0.06);
animation: fadeIn 1s ease-out 0.5s backwards;
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.25);
transition: all 0.4s ease;
&.is-active {
background: #30d158;
box-shadow:
0 0 8px rgba(48, 209, 88, 0.5),
0 0 16px rgba(48, 209, 88, 0.3);
animation: breathe 2.5s ease-in-out infinite;
}
}
.status-text {
font-size: 0.8125rem;
font-weight: 500;
color: rgba(255, 255, 255, 0.7);
letter-spacing: 0.01em;
}
// Animations
@keyframes fadeIn {
to {
opacity: 1;
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-8px);
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.9);
}
}
@keyframes breathe {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(0.9);
}
}
@keyframes pulse {
0%,
100% {
opacity: 0.5;
}
50% {
opacity: 0.8;
}
}
// Responsive
@media (max-width: 480px) {
.remote__logo {
font-size: 1.25rem;
}
.touchpad {
max-width: 320px;
}
.touchpad__button svg {
width: 48px;
height: 48px;
}
}
@media (max-width: 360px) {
.touchpad {
max-width: 280px;
}
.touchpad__button svg {
width: 42px;
height: 42px;
}
.touchpad__center {
width: 40px;
height: 40px;
}
.touchpad__pulse {
width: 14px;
height: 14px;
}
}
// Accessibility
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
@@ -0,0 +1,42 @@
import { CommonModule } from '@angular/common';
import { Component, inject } from '@angular/core';
import { RemoteControlService } from './remote-control.service';
@Component({
selector: 'lib-remote-control',
imports: [CommonModule],
templateUrl: './remote-control.component.html',
styleUrls: ['./remote-control.component.scss'],
})
export class RemoteControlComponent {
private remoteControlService = inject(RemoteControlService);
isLoading = false;
error: string | null = null;
async changeChannelUp(): Promise<void> {
this.isLoading = true;
this.error = null;
try {
await this.remoteControlService.channelUp();
} catch (err) {
this.error = 'Failed to change channel up';
console.error(err);
} finally {
this.isLoading = false;
}
}
async changeChannelDown(): Promise<void> {
this.isLoading = true;
this.error = null;
try {
await this.remoteControlService.channelDown();
} catch (err) {
this.error = 'Failed to change channel down';
console.error(err);
} finally {
this.isLoading = false;
}
}
}
@@ -0,0 +1,34 @@
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { firstValueFrom } from 'rxjs';
/**
* Service for remote control operations.
* This service communicates with the Electron backend via HTTP API.
*/
@Injectable({
providedIn: 'root',
})
export class RemoteControlService {
private readonly apiBaseUrl = '/api/remote-control';
constructor(private http: HttpClient) {}
/**
* Changes to the next channel
*/
async channelUp(): Promise<void> {
await firstValueFrom(
this.http.post<void>(`${this.apiBaseUrl}/channel/up`, {})
);
}
/**
* Changes to the previous channel
*/
async channelDown(): Promise<void> {
await firstValueFrom(
this.http.post<void>(`${this.apiBaseUrl}/channel/down`, {})
);
}
}
+6
View File
@@ -0,0 +1,6 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
setupZoneTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});
+30
View File
@@ -0,0 +1,30 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"target": "es2022",
"moduleResolution": "bundler",
"strict": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"module": "preserve"
},
"angularCompilerOptions": {
"enableI18nLegacyMessageIdFormat": false,
"strictInjectionParameters": true,
"strictInputAccessModifiers": true,
"typeCheckHostBindings": true,
"strictTemplates": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
+17
View File
@@ -0,0 +1,17 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"declaration": true,
"declarationMap": true,
"inlineSources": true,
"types": []
},
"exclude": [
"src/**/*.spec.ts",
"src/test-setup.ts",
"jest.config.ts",
"src/**/*.test.ts"
],
"include": ["src/**/*.ts"]
}
+17
View File
@@ -0,0 +1,17 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../../dist/out-tsc",
"module": "commonjs",
"target": "es2016",
"types": ["jest", "node"],
"moduleResolution": "node10"
},
"files": ["src/test-setup.ts"],
"include": [
"jest.config.ts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
+2 -1
View File
@@ -22,7 +22,8 @@
"m3u-utils": ["libs/shared/m3u-utils/src/index.ts"],
"services": ["libs/services/src/index.ts"],
"shared-interfaces": ["libs/shared/interfaces/src/index.ts"],
"shared-portals": ["libs/ui/shared-portals/src/index.ts"]
"shared-portals": ["libs/ui/shared-portals/src/index.ts"],
"remote-control": ["libs/ui/remote-control/src/index.ts"]
}
},
"exclude": ["node_modules", "tmp"],