mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-13 04:26:16 -08:00
Merge pull request #652 from 4gray/feat-remote-control-ui-server-integration
feat-remote-control-ui-server-integration
This commit is contained in:
49 files changed
+2407
-790
No files matched your search
@@ -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();
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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.
|
||||
@@ -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: {},
|
||||
},
|
||||
];
|
||||
@@ -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',
|
||||
],
|
||||
};
|
||||
@@ -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],
|
||||
},
|
||||
})
|
||||
),
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
<lib-remote-control />
|
||||
@@ -0,0 +1,4 @@
|
||||
:host {
|
||||
display: block;
|
||||
height: 100vh;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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));
|
||||
@@ -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,
|
||||
});
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
+3
-3
@@ -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)
|
||||
|
||||
+7
-4
@@ -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'
|
||||
);
|
||||
@@ -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.
|
||||
@@ -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: {},
|
||||
},
|
||||
];
|
||||
@@ -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',
|
||||
],
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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`, {})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
|
||||
|
||||
setupZoneTestEnv({
|
||||
errorOnUnknownElements: true,
|
||||
errorOnUnknownProperties: true,
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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
@@ -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"],
|
||||
|
||||
Reference in new issue
Block a user