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 000000000..317ebcb23 Binary files /dev/null and b/apps/remote-control-web/public/favicon.ico differ diff --git a/apps/remote-control-web/src/app/app.config.ts b/apps/remote-control-web/src/app/app.config.ts new file mode 100644 index 000000000..ad40068af --- /dev/null +++ b/apps/remote-control-web/src/app/app.config.ts @@ -0,0 +1,32 @@ +import { + ApplicationConfig, + importProvidersFrom, + provideBrowserGlobalErrorListeners, + provideZoneChangeDetection, +} from '@angular/core'; +import { provideHttpClient } from '@angular/common/http'; +import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; +import { TranslateHttpLoader } from '@ngx-translate/http-loader'; +import { HttpClient } from '@angular/common/http'; + +export function HttpLoaderFactory(http: HttpClient) { + return new TranslateHttpLoader(http, './assets/i18n/', '.json'); +} + +export const appConfig: ApplicationConfig = { + providers: [ + provideBrowserGlobalErrorListeners(), + provideZoneChangeDetection({ eventCoalescing: true }), + provideHttpClient(), + importProvidersFrom( + TranslateModule.forRoot({ + defaultLanguage: 'en', + loader: { + provide: TranslateLoader, + useFactory: HttpLoaderFactory, + deps: [HttpClient], + }, + }) + ), + ], +}; diff --git a/apps/remote-control-web/src/app/app.html b/apps/remote-control-web/src/app/app.html new file mode 100644 index 000000000..1051dd18d --- /dev/null +++ b/apps/remote-control-web/src/app/app.html @@ -0,0 +1 @@ + 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"],