From 028ccf1580f346eba29bc95e14a6274d32e4f6da Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 16 Nov 2025 17:36:39 +0100 Subject: [PATCH 1/7] feat(ui/components): export and reuse overlay injection token Introduce a shared InjectionToken for OverlayRef used by the multi-EPG overlay component and export it from the components module. - Add overlay-ref.token.ts defining COMPONENT_OVERLAY_REF to provide OverlayRef for dynamically created overlay components. - Replace a locally created InjectionToken with an injected COMPONENT_OVERLAY_REF in multi-epg-container.component to avoid token duplication and ensure consistent DI across modules. - Export the token from libs/ui/components/src/index.ts so other consumers can import and use the same token. This reduces duplicated token definitions, fixes DI inconsistencies, and makes the overlay ref reusable by external components. --- libs/ui/components/src/index.ts | 1 + .../src/lib/multi-epg/multi-epg-container.component.ts | 10 ++-------- .../components/src/lib/multi-epg/overlay-ref.token.ts | 9 +++++++++ 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 libs/ui/components/src/lib/multi-epg/overlay-ref.token.ts diff --git a/libs/ui/components/src/index.ts b/libs/ui/components/src/index.ts index d5c46c97a..2c1817d76 100644 --- a/libs/ui/components/src/index.ts +++ b/libs/ui/components/src/index.ts @@ -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'; diff --git a/libs/ui/components/src/lib/multi-epg/multi-epg-container.component.ts b/libs/ui/components/src/lib/multi-epg/multi-epg-container.component.ts index 2bfe0b177..3dfcc68d1 100644 --- a/libs/ui/components/src/lib/multi-epg/multi-epg-container.component.ts +++ b/libs/ui/components/src/lib/multi-epg/multi-epg-container.component.ts @@ -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( - 'COMPONENT_OVERLAY_REF' - ); - - private readonly overlayRef = inject( - this.COMPONENT_OVERLAY_REF - ); + private readonly overlayRef = inject(COMPONENT_OVERLAY_REF); ngOnInit() { this.calculateCurrentTimeBar(); diff --git a/libs/ui/components/src/lib/multi-epg/overlay-ref.token.ts b/libs/ui/components/src/lib/multi-epg/overlay-ref.token.ts new file mode 100644 index 000000000..db768fed1 --- /dev/null +++ b/libs/ui/components/src/lib/multi-epg/overlay-ref.token.ts @@ -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( + 'COMPONENT_OVERLAY_REF' +); From 79b1e991ef72cbd23d90627984dd8d6017c0164e Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 16 Nov 2025 17:41:50 +0100 Subject: [PATCH 2/7] feat(remote-control): add remote control UI library Add a new library libs/ui/remote-control including component, template, Jest setup, and project configuration to provide a remote-control UI component. - Add RemoteControlComponent implemented with inject-based service usage, loading and error state, and async channelUp/channelDown handlers. This centralizes remote control logic and handles errors for the UI. - Add HTML template for the remote-control touchpad, buttons, error display, and basic branding markup. - Add Jest config (jest.config.ts) and tsconfig.spec.json configured for unit tests with jest-preset-angular to enable testing of Angular components. - Add Nx project configuration (project.json) with test and lint targets so the library is discoverable and runnable by the workspace. These changes scaffold the remote-control feature as a reusable UI library and enable testing and linting within the monorepo. --- libs/ui/remote-control/README.md | 7 + libs/ui/remote-control/eslint.config.mjs | 34 ++ libs/ui/remote-control/jest.config.ts | 21 + libs/ui/remote-control/project.json | 21 + libs/ui/remote-control/src/index.ts | 2 + .../remote-control.component.html | 77 ++++ .../remote-control.component.scss | 398 ++++++++++++++++++ .../remote-control.component.ts | 42 ++ .../remote-control/remote-control.service.ts | 34 ++ libs/ui/remote-control/src/test-setup.ts | 6 + libs/ui/remote-control/tsconfig.json | 30 ++ libs/ui/remote-control/tsconfig.lib.json | 17 + libs/ui/remote-control/tsconfig.spec.json | 17 + 13 files changed, 706 insertions(+) create mode 100644 libs/ui/remote-control/README.md create mode 100644 libs/ui/remote-control/eslint.config.mjs create mode 100644 libs/ui/remote-control/jest.config.ts create mode 100644 libs/ui/remote-control/project.json create mode 100644 libs/ui/remote-control/src/index.ts create mode 100644 libs/ui/remote-control/src/lib/remote-control/remote-control.component.html create mode 100644 libs/ui/remote-control/src/lib/remote-control/remote-control.component.scss create mode 100644 libs/ui/remote-control/src/lib/remote-control/remote-control.component.ts create mode 100644 libs/ui/remote-control/src/lib/remote-control/remote-control.service.ts create mode 100644 libs/ui/remote-control/src/test-setup.ts create mode 100644 libs/ui/remote-control/tsconfig.json create mode 100644 libs/ui/remote-control/tsconfig.lib.json create mode 100644 libs/ui/remote-control/tsconfig.spec.json diff --git a/libs/ui/remote-control/README.md b/libs/ui/remote-control/README.md new file mode 100644 index 000000000..0630db4e9 --- /dev/null +++ b/libs/ui/remote-control/README.md @@ -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. diff --git a/libs/ui/remote-control/eslint.config.mjs b/libs/ui/remote-control/eslint.config.mjs new file mode 100644 index 000000000..937c5f406 --- /dev/null +++ b/libs/ui/remote-control/eslint.config.mjs @@ -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: {}, + }, +]; diff --git a/libs/ui/remote-control/jest.config.ts b/libs/ui/remote-control/jest.config.ts new file mode 100644 index 000000000..395a66fa9 --- /dev/null +++ b/libs/ui/remote-control/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'remote-control', + preset: '../../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../../coverage/libs/ui/remote-control', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/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', + ], +}; diff --git a/libs/ui/remote-control/project.json b/libs/ui/remote-control/project.json new file mode 100644 index 000000000..d5257af8e --- /dev/null +++ b/libs/ui/remote-control/project.json @@ -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" + } + } +} diff --git a/libs/ui/remote-control/src/index.ts b/libs/ui/remote-control/src/index.ts new file mode 100644 index 000000000..a2f5fa64c --- /dev/null +++ b/libs/ui/remote-control/src/index.ts @@ -0,0 +1,2 @@ +export * from './lib/remote-control/remote-control.component'; +export * from './lib/remote-control/remote-control.service'; diff --git a/libs/ui/remote-control/src/lib/remote-control/remote-control.component.html b/libs/ui/remote-control/src/lib/remote-control/remote-control.component.html new file mode 100644 index 000000000..bcfc57fb2 --- /dev/null +++ b/libs/ui/remote-control/src/lib/remote-control/remote-control.component.html @@ -0,0 +1,77 @@ +
+
+ +
+ +
+ + + @if (error) { +
+ {{ error }} +
+ } + + +
+
+ + + + + + + +
+
+
+
+ + +
+
+ + +
+
+ {{ + isLoading ? 'Switching' : 'Ready' + }} +
+
+
diff --git a/libs/ui/remote-control/src/lib/remote-control/remote-control.component.scss b/libs/ui/remote-control/src/lib/remote-control/remote-control.component.scss new file mode 100644 index 000000000..4f73c25aa --- /dev/null +++ b/libs/ui/remote-control/src/lib/remote-control/remote-control.component.scss @@ -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; + } +} diff --git a/libs/ui/remote-control/src/lib/remote-control/remote-control.component.ts b/libs/ui/remote-control/src/lib/remote-control/remote-control.component.ts new file mode 100644 index 000000000..eb390caf7 --- /dev/null +++ b/libs/ui/remote-control/src/lib/remote-control/remote-control.component.ts @@ -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 { + 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 { + 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; + } + } +} diff --git a/libs/ui/remote-control/src/lib/remote-control/remote-control.service.ts b/libs/ui/remote-control/src/lib/remote-control/remote-control.service.ts new file mode 100644 index 000000000..8151ae401 --- /dev/null +++ b/libs/ui/remote-control/src/lib/remote-control/remote-control.service.ts @@ -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 { + await firstValueFrom( + this.http.post(`${this.apiBaseUrl}/channel/up`, {}) + ); + } + + /** + * Changes to the previous channel + */ + async channelDown(): Promise { + await firstValueFrom( + this.http.post(`${this.apiBaseUrl}/channel/down`, {}) + ); + } +} diff --git a/libs/ui/remote-control/src/test-setup.ts b/libs/ui/remote-control/src/test-setup.ts new file mode 100644 index 000000000..0d88648e3 --- /dev/null +++ b/libs/ui/remote-control/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/libs/ui/remote-control/tsconfig.json b/libs/ui/remote-control/tsconfig.json new file mode 100644 index 000000000..e05028a82 --- /dev/null +++ b/libs/ui/remote-control/tsconfig.json @@ -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" + } + ] +} diff --git a/libs/ui/remote-control/tsconfig.lib.json b/libs/ui/remote-control/tsconfig.lib.json new file mode 100644 index 000000000..8eb88c679 --- /dev/null +++ b/libs/ui/remote-control/tsconfig.lib.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"] +} diff --git a/libs/ui/remote-control/tsconfig.spec.json b/libs/ui/remote-control/tsconfig.spec.json new file mode 100644 index 000000000..7b3e56bde --- /dev/null +++ b/libs/ui/remote-control/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" + ] +} From 5d2875e5544926293c9768e0c254477325d33642 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 16 Nov 2025 17:43:03 +0100 Subject: [PATCH 3/7] feat(remote-control-web): add new web app for remote control Add a new remote-control-web application using Angular standalone bootstrap. Add main.ts to bootstrap Application with appConfig and log bootstrap errors. Introduce global styles (styles.scss) to reset page margins, ensure full-height layout, set dark background, and prevent horizontal scroll. Add Nx project configuration (project.json) for build, serve, test, lint, extract-i18n and serve-static targets with production and development configurations, asset and style wiring, and Jest test integration. Add tsconfig.spec.json to configure testing compilation (Jest, Node), include test files and test setup. Start app configuration provider file (app.config.ts) to prepare application providers (partial file added). These changes scaffold the new remote-control-web application and enable building, serving and testing in the monorepo. --- apps/electron-backend/project.json | 3 +- apps/remote-control-web/README.md | 174 ++++++++++++++++++ apps/remote-control-web/eslint.config.mjs | 34 ++++ apps/remote-control-web/jest.config.ts | 21 +++ apps/remote-control-web/project.json | 96 ++++++++++ apps/remote-control-web/public/favicon.ico | Bin 0 -> 15086 bytes apps/remote-control-web/src/app/app.config.ts | 32 ++++ apps/remote-control-web/src/app/app.html | 1 + apps/remote-control-web/src/app/app.scss | 4 + apps/remote-control-web/src/app/app.ts | 19 ++ apps/remote-control-web/src/index.html | 13 ++ apps/remote-control-web/src/main.ts | 5 + apps/remote-control-web/src/styles.scss | 11 ++ apps/remote-control-web/src/test-setup.ts | 6 + apps/remote-control-web/tsconfig.app.json | 14 ++ apps/remote-control-web/tsconfig.json | 32 ++++ apps/remote-control-web/tsconfig.spec.json | 17 ++ tsconfig.base.json | 3 +- 18 files changed, 483 insertions(+), 2 deletions(-) create mode 100644 apps/remote-control-web/README.md create mode 100644 apps/remote-control-web/eslint.config.mjs create mode 100644 apps/remote-control-web/jest.config.ts create mode 100644 apps/remote-control-web/project.json create mode 100644 apps/remote-control-web/public/favicon.ico create mode 100644 apps/remote-control-web/src/app/app.config.ts create mode 100644 apps/remote-control-web/src/app/app.html create mode 100644 apps/remote-control-web/src/app/app.scss create mode 100644 apps/remote-control-web/src/app/app.ts create mode 100644 apps/remote-control-web/src/index.html create mode 100644 apps/remote-control-web/src/main.ts create mode 100644 apps/remote-control-web/src/styles.scss create mode 100644 apps/remote-control-web/src/test-setup.ts create mode 100644 apps/remote-control-web/tsconfig.app.json create mode 100644 apps/remote-control-web/tsconfig.json create mode 100644 apps/remote-control-web/tsconfig.spec.json diff --git a/apps/electron-backend/project.json b/apps/electron-backend/project.json index 152ef13d8..d03e3a92c 100644 --- a/apps/electron-backend/project.json +++ b/apps/electron-backend/project.json @@ -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", diff --git a/apps/remote-control-web/README.md b/apps/remote-control-web/README.md new file mode 100644 index 000000000..9137bddae --- /dev/null +++ b/apps/remote-control-web/README.md @@ -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://: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. diff --git a/apps/remote-control-web/eslint.config.mjs b/apps/remote-control-web/eslint.config.mjs new file mode 100644 index 000000000..43bac6ec9 --- /dev/null +++ b/apps/remote-control-web/eslint.config.mjs @@ -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: {}, + }, +]; diff --git a/apps/remote-control-web/jest.config.ts b/apps/remote-control-web/jest.config.ts new file mode 100644 index 000000000..98cc58074 --- /dev/null +++ b/apps/remote-control-web/jest.config.ts @@ -0,0 +1,21 @@ +export default { + displayName: 'remote-control-web', + preset: '../../jest.preset.js', + setupFilesAfterEnv: ['/src/test-setup.ts'], + coverageDirectory: '../../coverage/apps/remote-control-web', + transform: { + '^.+\\.(ts|mjs|js|html)$': [ + 'jest-preset-angular', + { + tsconfig: '/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', + ], +}; diff --git a/apps/remote-control-web/project.json b/apps/remote-control-web/project.json new file mode 100644 index 000000000..9479337a7 --- /dev/null +++ b/apps/remote-control-web/project.json @@ -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 + } + } + } +} diff --git a/apps/remote-control-web/public/favicon.ico b/apps/remote-control-web/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..317ebcb2336e0833a22dddf0ab287849f26fda57 GIT binary patch literal 15086 zcmeI332;U^%p|z7g|#(P)qFEA@4f!_@qOK2 z_lJl}!lhL!VT_U|uN7%8B2iKH??xhDa;*`g{yjTFWHvXn;2s{4R7kH|pKGdy(7z!K zgftM+Ku7~24TLlh(!g)gz|foI94G^t2^IO$uvX$3(OR0<_5L2sB)lMAMy|+`xodJ{ z_Uh_1m)~h?a;2W{dmhM;u!YGo=)OdmId_B<%^V^{ovI@y`7^g1_V9G}*f# zNzAtvou}I!W1#{M^@ROc(BZ! z+F!!_aR&Px3_reO(EW+TwlW~tv*2zr?iP7(d~a~yA|@*a89IUke+c472NXM0wiX{- zl`UrZC^1XYyf%1u)-Y)jj9;MZ!SLfd2Hl?o|80Su%Z?To_=^g_Jt0oa#CT*tjx>BI z16wec&AOWNK<#i0Qd=1O$fymLRoUR*%;h@*@v7}wApDl^w*h}!sYq%kw+DKDY)@&A z@9$ULEB3qkR#85`lb8#WZw=@})#kQig9oqy^I$dj&k4jU&^2(M3q{n1AKeGUKPFbr z1^<)aH;VsG@J|B&l>UtU#Ejv3GIqERzYgL@UOAWtW<{p#zy`WyJgpCy8$c_e%wYJL zyGHRRx38)HyjU3y{-4z6)pzb>&Q1pR)B&u01F-|&Gx4EZWK$nkUkOI|(D4UHOXg_- zw{OBf!oWQUn)Pe(=f=nt=zkmdjpO^o8ZZ9o_|4tW1ni+Un9iCW47*-ut$KQOww!;u z`0q)$s6IZO!~9$e_P9X!hqLxu`fpcL|2f^I5d4*a@Dq28;@2271v_N+5HqYZ>x;&O z05*7JT)mUe&%S0@UD)@&8SmQrMtsDfZT;fkdA!r(S=}Oz>iP)w=W508=Rc#nNn7ym z1;42c|8($ALY8#a({%1#IXbWn9-Y|0eDY$_L&j{63?{?AH{);EzcqfydD$@-B`Y3<%IIj7S7rK_N}je^=dEk%JQ4c z!tBdTPE3Tse;oYF>cnrapWq*o)m47X1`~6@(!Y29#>-#8zm&LXrXa(3=7Z)ElaQqj z-#0JJy3Fi(C#Rx(`=VXtJ63E2_bZGCz+QRa{W0e2(m3sI?LOcUBx)~^YCqZ{XEPX)C>G>U4tfqeH8L(3|pQR*zbL1 zT9e~4Tb5p9_G}$y4t`i*4t_Mr9QYvL9C&Ah*}t`q*}S+VYh0M6GxTTSXI)hMpMpIq zD1ImYqJLzbj0}~EpE-aH#VCH_udYEW#`P2zYmi&xSPs_{n6tBj=MY|-XrA;SGA_>y zGtU$?HXm$gYj*!N)_nQ59%lQdXtQZS3*#PC-{iB_sm+ytD*7j`D*k(P&IH2GHT}Eh z5697eQECVIGQAUe#eU2I!yI&%0CP#>%6MWV z@zS!p@+Y1i1b^QuuEF*13CuB zu69dve5k7&Wgb+^s|UB08Dr3u`h@yM0NTj4h7MnHo-4@xmyr7(*4$rpPwsCDZ@2be zRz9V^GnV;;?^Lk%ynzq&K(Aix`mWmW`^152Hoy$CTYVehpD-S1-W^#k#{0^L`V6CN+E z!w+xte;2vu4AmVNEFUOBmrBL>6MK@!O2*N|2=d|Y;oN&A&qv=qKn73lDD zI(+oJAdgv>Yr}8(&@ZuAZE%XUXmX(U!N+Z_sjL<1vjy1R+1IeHt`79fnYdOL{$ci7 z%3f0A*;Zt@ED&Gjm|OFTYBDe%bbo*xXAQsFz+Q`fVBH!N2)kaxN8P$c>sp~QXnv>b zwq=W3&Mtmih7xkR$YA)1Yi?avHNR6C99!u6fh=cL|KQ&PwF!n@ud^n(HNIImHD!h87!i*t?G|p0o+eelJ?B@A64_9%SBhNaJ64EvKgD&%LjLCYnNfc; znj?%*p@*?dq#NqcQFmmX($wms@CSAr9#>hUR^=I+=0B)vvGX%T&#h$kmX*s=^M2E!@N9#m?LhMvz}YB+kd zG~mbP|D(;{s_#;hsKK9lbVK&Lo734x7SIFJ9V_}2$@q?zm^7?*XH94w5Qae{7zOMUF z^?%F%)c1Y)Q?Iy?I>knw*8gYW#ok|2gdS=YYZLiD=CW|Nj;n^x!=S#iJ#`~Ld79+xXpVmUK^B(xO_vO!btA9y7w3L3-0j-y4 z?M-V{%z;JI`bk7yFDcP}OcCd*{Q9S5$iGA7*E1@tfkyjAi!;wP^O71cZ^Ep)qrQ)N z#wqw0_HS;T7x3y|`P==i3hEwK%|>fZ)c&@kgKO1~5<5xBSk?iZV?KI6&i72H6S9A* z=U(*e)EqEs?Oc04)V-~K5AUmh|62H4*`UAtItO$O(q5?6jj+K^oD!04r=6#dsxp?~}{`?&sXn#q2 zGuY~7>O2=!u@@Kfu7q=W*4egu@qPMRM>(eyYyaIE<|j%d=iWNdGsx%c!902v#ngNg z@#U-O_4xN$s_9?(`{>{>7~-6FgWpBpqXb`Ydc3OFL#&I}Irse9F_8R@4zSS*Y*o*B zXL?6*Aw!AfkNCgcr#*yj&p3ZDe2y>v$>FUdKIy_2N~}6AbHc7gA3`6$g@1o|dE>vz z4pl(j9;kyMsjaw}lO?(?Xg%4k!5%^t#@5n=WVc&JRa+XT$~#@rldvN3S1rEpU$;XgxVny7mki3 z-Hh|jUCHrUXuLr!)`w>wgO0N%KTB-1di>cj(x3Bav`7v z3G7EIbU$z>`Nad7Rk_&OT-W{;qg)-GXV-aJT#(ozdmnA~Rq3GQ_3mby(>q6Ocb-RgTUhTN)))x>m&eD;$J5Bg zo&DhY36Yg=J=$Z>t}RJ>o|@hAcwWzN#r(WJ52^g$lh^!63@hh+dR$&_dEGu&^CR*< z!oFqSqO@>xZ*nC2oiOd0eS*F^IL~W-rsrO`J`ej{=ou_q^_(<$&-3f^J z&L^MSYWIe{&pYq&9eGaArA~*kA diff --git a/apps/remote-control-web/src/app/app.scss b/apps/remote-control-web/src/app/app.scss new file mode 100644 index 000000000..f85909345 --- /dev/null +++ b/apps/remote-control-web/src/app/app.scss @@ -0,0 +1,4 @@ +:host { + display: block; + height: 100vh; +} diff --git a/apps/remote-control-web/src/app/app.ts b/apps/remote-control-web/src/app/app.ts new file mode 100644 index 000000000..0ba58f221 --- /dev/null +++ b/apps/remote-control-web/src/app/app.ts @@ -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'); + } +} diff --git a/apps/remote-control-web/src/index.html b/apps/remote-control-web/src/index.html new file mode 100644 index 000000000..69b4ac527 --- /dev/null +++ b/apps/remote-control-web/src/index.html @@ -0,0 +1,13 @@ + + + + + remote-control-web + + + + + + + + diff --git a/apps/remote-control-web/src/main.ts b/apps/remote-control-web/src/main.ts new file mode 100644 index 000000000..190f3418d --- /dev/null +++ b/apps/remote-control-web/src/main.ts @@ -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)); diff --git a/apps/remote-control-web/src/styles.scss b/apps/remote-control-web/src/styles.scss new file mode 100644 index 000000000..57526dcdc --- /dev/null +++ b/apps/remote-control-web/src/styles.scss @@ -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; +} diff --git a/apps/remote-control-web/src/test-setup.ts b/apps/remote-control-web/src/test-setup.ts new file mode 100644 index 000000000..0d88648e3 --- /dev/null +++ b/apps/remote-control-web/src/test-setup.ts @@ -0,0 +1,6 @@ +import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone'; + +setupZoneTestEnv({ + errorOnUnknownElements: true, + errorOnUnknownProperties: true, +}); diff --git a/apps/remote-control-web/tsconfig.app.json b/apps/remote-control-web/tsconfig.app.json new file mode 100644 index 000000000..231df7c01 --- /dev/null +++ b/apps/remote-control-web/tsconfig.app.json @@ -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" + ] +} diff --git a/apps/remote-control-web/tsconfig.json b/apps/remote-control-web/tsconfig.json new file mode 100644 index 000000000..b81a1f7df --- /dev/null +++ b/apps/remote-control-web/tsconfig.json @@ -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" + } + ] +} diff --git a/apps/remote-control-web/tsconfig.spec.json b/apps/remote-control-web/tsconfig.spec.json new file mode 100644 index 000000000..cc2617958 --- /dev/null +++ b/apps/remote-control-web/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" + ] +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 44bf52130..2219622e4 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -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"], From 5fa44234008d8be34186ee13a7aaa73ef75ba0c5 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 22 Nov 2025 08:20:56 +0100 Subject: [PATCH 4/7] feat(settings): enable desktop remote control and change port Update default remote control port from 3000 to 8765 in settings store and component initialization to avoid conflicts with common development ports and align with the desktop/tauri environment. Unhide and scope the remote control UI to desktop builds only: - Restore the remote control block in settings template and guard it with @if (isDesktop) so the checkbox and description render for desktop users (previously commented out as not ported). - Keep the remote control settings section conditionally visible when the remoteControl form value is true. These changes ensure desktop users can configure and enable the remote control feature with a non-conflicting default port. --- .../app/services/settings-store.service.ts | 2 +- .../src/app/settings/settings.component.html | 52 ++++++++++++------- .../src/app/settings/settings.component.ts | 10 +++- 3 files changed, 43 insertions(+), 21 deletions(-) diff --git a/apps/web/src/app/services/settings-store.service.ts b/apps/web/src/app/services/settings-store.service.ts index 6215f28dc..4aed192ab 100644 --- a/apps/web/src/app/services/settings-store.service.ts +++ b/apps/web/src/app/services/settings-store.service.ts @@ -27,7 +27,7 @@ const DEFAULT_SETTINGS: Settings = { mpvReuseInstance: false, vlcPlayerPath: '', remoteControl: false, - remoteControlPort: 3000, + remoteControlPort: 8765, epgUrl: [], }; diff --git a/apps/web/src/app/settings/settings.component.html b/apps/web/src/app/settings/settings.component.html index e483a4468..f882c0261 100644 --- a/apps/web/src/app/settings/settings.component.html +++ b/apps/web/src/app/settings/settings.component.html @@ -270,25 +270,23 @@ > - @if (settingsForm.value.remoteControl === true) {
@@ -302,13 +300,29 @@

- + + Port (1-65535) + @if (settingsForm.get('remoteControlPort')?.hasError('required')) { + {{ 'SETTINGS.PORT_REQUIRED' | translate }} + } + @if (settingsForm.get('remoteControlPort')?.hasError('min')) { + {{ 'SETTINGS.PORT_MIN' | translate }} + } + @if (settingsForm.get('remoteControlPort')?.hasError('max')) { + {{ 'SETTINGS.PORT_MAX' | translate }} + } + @if (settingsForm.get('remoteControlPort')?.hasError('pattern')) { + {{ 'SETTINGS.PORT_PATTERN' | translate }} + }
diff --git a/apps/web/src/app/settings/settings.component.ts b/apps/web/src/app/settings/settings.component.ts index 5bc12672f..0d3b86897 100644 --- a/apps/web/src/app/settings/settings.component.ts +++ b/apps/web/src/app/settings/settings.component.ts @@ -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 */ From a48f7d60be6eca1f2b855fc7b1eddfc8b62e12c1 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 16 Nov 2025 21:41:35 +0100 Subject: [PATCH 5/7] feat(electron-backend): add lightweight HTTP server for remote UI Add a new HttpServer class that serves the built remote-control-web static assets and provides a simple REST/API routing layer for remote control endpoints. Key changes: - Implement server lifecycle: start, stop, updateSettings with port and enabled handling. - Resolve distribution path depending on electron packaging (dev vs production). - Serve static files with basic security (normalize path to prevent traversal) and SPA fallback to index.html for client-side routing. - Provide registration API for remote control handlers mapped to /api/remote-control/* endpoints. - Log server status and listening URL on start, and on stop. These changes enable the Electron backend to host the remote control web app and route API calls without external web servers, simplifying development and packaging. --- .../src/app/api/main.preload.ts | 4 + .../src/app/events/remote-control.events.ts | 107 ++++++++++ .../src/app/events/settings.events.ts | 20 +- .../src/app/server/http-server.ts | 200 ++++++++++++++++++ apps/electron-backend/src/main.ts | 2 + global.d.ts | 4 + 6 files changed, 336 insertions(+), 1 deletion(-) create mode 100644 apps/electron-backend/src/app/events/remote-control.events.ts create mode 100644 apps/electron-backend/src/app/server/http-server.ts diff --git a/apps/electron-backend/src/app/api/main.preload.ts b/apps/electron-backend/src/app/api/main.preload.ts index de1012256..b91fafabb 100644 --- a/apps/electron-backend/src/app/api/main.preload.ts +++ b/apps/electron-backend/src/app/api/main.preload.ts @@ -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) => diff --git a/apps/electron-backend/src/app/events/remote-control.events.ts b/apps/electron-backend/src/app/events/remote-control.events.ts new file mode 100644 index 000000000..90c6e7a10 --- /dev/null +++ b/apps/electron-backend/src/app/events/remote-control.events.ts @@ -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(); diff --git a/apps/electron-backend/src/app/events/settings.events.ts b/apps/electron-backend/src/app/events/settings.events.ts index 692b266d0..4d50233dc 100644 --- a/apps/electron-backend/src/app/events/settings.events.ts +++ b/apps/electron-backend/src/app/events/settings.events.ts @@ -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); + } }); diff --git a/apps/electron-backend/src/app/server/http-server.ts b/apps/electron-backend/src/app/server/http-server.ts new file mode 100644 index 000000000..c56b38d2b --- /dev/null +++ b/apps/electron-backend/src/app/server/http-server.ts @@ -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 = { + '.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(); diff --git a/apps/electron-backend/src/main.ts b/apps/electron-backend/src/main.ts index 4e1d08ed6..69d6f8e9b 100644 --- a/apps/electron-backend/src/main.ts +++ b/apps/electron-backend/src/main.ts @@ -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()) { diff --git a/global.d.ts b/global.d.ts index bacf1bde3..199d2292e 100644 --- a/global.d.ts +++ b/global.d.ts @@ -156,6 +156,10 @@ declare global { xtreamId: number, playlistId: string ) => Promise; + // Remote control + onChannelChange?: ( + callback: (data: { direction: 'up' | 'down' }) => void + ) => void; }; process: NodeJS.Process; require: NodeRequire; From c8961c94d5676c8198ec546e83c496709f3952ba Mon Sep 17 00:00:00 2001 From: 4gray Date: Sun, 16 Nov 2025 17:45:18 +0100 Subject: [PATCH 6/7] feat: use signal for active channel + listen for remote control events - Replace local selected state with store signal (selectActive) in the channel list container so templates bind to activeChannel()?.url instead of comparing selected?.id. This unifies selection handling with the global store and prevents stale local state. - Update selectChannel to dispatch setActiveChannel instead of mutating local selected property; keep EPG lookup behavior intact. - Import selectActive alongside other selectors from m3u-state and remove unused local InjectionToken in video player. - Add remote control listener in VideoPlayer (Electron) to handle channel up/down events and wire a new handleRemoteChannelChange method skeleton. Use combineLatest/take to safely read required observables for channel switching. - Minor rxjs import and formatting adjustments. These changes centralize active channel state in the store, simplify template logic, and add support for remote channel changes on desktop. --- .../video-player/video-player.component.ts | 67 +++++++++++++++++-- .../channel-list-container.component.html | 6 +- .../channel-list-container.component.ts | 11 +-- 3 files changed, 70 insertions(+), 14 deletions(-) diff --git a/apps/web/src/app/home/video-player/video-player.component.ts b/apps/web/src/app/home/video-player/video-player.component.ts index 19d2aaf7b..e58d41192 100644 --- a/apps/web/src/app/home/video-player/video-player.component.ts +++ b/apps/web/src/app/home/video-player/video-player.component.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( - '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, }, ], diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html index 1c0a2e4bc..8414827f7 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.html @@ -42,7 +42,7 @@ " [logo]="channel?.tvg?.logo" (clicked)="selectChannel(channel)" - [selected]="selected?.id === channel?.id" + [selected]="activeChannel()?.url === channel?.url" /> @@ -90,7 +90,7 @@ [logo]="channel?.tvg?.logo" (clicked)="selectChannel(channel)" [selected]=" - selected?.id === channel.id + activeChannel()?.url === channel.url " > } @@ -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) diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts index 079d9d8cd..2c74f3747 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts @@ -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 From 63f80064d65ceb1a35b79f63b74bca96f5be65a2 Mon Sep 17 00:00:00 2001 From: 4gray Date: Sat, 22 Nov 2025 08:20:38 +0100 Subject: [PATCH 7/7] refactor(i18n): update remote control copy and default port Update English translations for the remote-control UI and change the documented default remote-control port. - Change default remote control port description from 3000 to 8765 to reflect actual configuration. - Improve remote control UI copy: - Add SUBTITLE "Control your IPTV player remotely". - Capitalize UP_CHANNEL and DOWN_CHANNEL labels to "Channel Up" and "Channel Down" for consistent UI presentation. These changes clarify the feature and align text with current behavior. --- apps/web/src/assets/i18n/de.json | 754 ++++++++++++++++--------------- apps/web/src/assets/i18n/en.json | 11 +- apps/web/src/assets/i18n/ru.json | 736 +++++++++++++++--------------- 3 files changed, 757 insertions(+), 744 deletions(-) diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index 961242124..6b283de03 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -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" - } -} \ No newline at end of file +} diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index b587a74a5..e50568ceb 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -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" } diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index a8a8a8a14..453258db7 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -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 Дистанционное управление" - } -} \ No newline at end of file +}