-
-
Notifications
You must be signed in to change notification settings - Fork 653
feat-remote-control-ui-server-integration #652
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
028ccf1
feat(ui/components): export and reuse overlay injection token
4gray 79b1e99
feat(remote-control): add remote control UI library
4gray 5d2875e
feat(remote-control-web): add new web app for remote control
4gray 5fa4423
feat(settings): enable desktop remote control and change port
4gray a48f7d6
feat(electron-backend): add lightweight HTTP server for remote UI
4gray c8961c9
feat: use signal for active channel + listen for remote control events
4gray 63f8006
refactor(i18n): update remote control copy and default port
4gray File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
107 changes: 107 additions & 0 deletions
107
apps/electron-backend/src/app/events/remote-control.events.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,200 @@ | ||
| import { app } from 'electron'; | ||
| import * as fs from 'fs'; | ||
| import * as http from 'http'; | ||
| import * as path from 'path'; | ||
|
|
||
| /** | ||
| * HTTP server for serving the remote control web app and providing REST API endpoints | ||
| */ | ||
| export class HttpServer { | ||
| private server: http.Server | null = null; | ||
| private port = 8765; | ||
| private isEnabled = false; | ||
| private distPath: string; | ||
| private remoteControlHandlers: Map< | ||
| string, | ||
| (req: http.IncomingMessage, res: http.ServerResponse) => void | ||
| > = new Map(); | ||
|
|
||
| constructor() { | ||
| // Path to the built remote-control-web app | ||
| // In development: use workspace root | ||
| // In production: use app path | ||
| const appPath = app.getAppPath(); | ||
| const isDev = !app.isPackaged; | ||
|
|
||
| if (isDev) { | ||
| // Development mode - use workspace root | ||
| this.distPath = path.join( | ||
| process.cwd(), | ||
| 'dist', | ||
| 'apps', | ||
| 'remote-control-web', | ||
| 'browser' | ||
| ); | ||
| } else { | ||
| // Production mode - files are bundled with the app | ||
| this.distPath = path.join( | ||
| appPath, | ||
| 'dist', | ||
| 'apps', | ||
| 'remote-control-web', | ||
| 'browser' | ||
| ); | ||
| } | ||
|
|
||
| console.log('[HTTP Server] Serving from:', this.distPath); | ||
| } | ||
|
|
||
| /** | ||
| * Start the HTTP server | ||
| */ | ||
| start(port?: number): void { | ||
| if (port) { | ||
| this.port = port; | ||
| } | ||
|
|
||
| if (this.server) { | ||
| console.log('HTTP server is already running'); | ||
| return; | ||
| } | ||
|
|
||
| this.server = http.createServer((req, res) => { | ||
| this.handleRequest(req, res); | ||
| }); | ||
|
|
||
| this.server.listen(this.port, () => { | ||
| console.log(`HTTP server listening on port ${this.port}`); | ||
| console.log( | ||
| `Remote control available at: http://localhost:${this.port}` | ||
| ); | ||
| }); | ||
|
|
||
| this.isEnabled = true; | ||
| } | ||
|
|
||
| /** | ||
| * Stop the HTTP server | ||
| */ | ||
| stop(): void { | ||
| if (!this.server) { | ||
| return; | ||
| } | ||
|
|
||
| this.server.close(() => { | ||
| console.log('HTTP server stopped'); | ||
| }); | ||
|
|
||
| this.server = null; | ||
| this.isEnabled = false; | ||
| } | ||
|
|
||
| /** | ||
| * Update server settings | ||
| */ | ||
| updateSettings(enabled: boolean, port: number): void { | ||
| const needsRestart = this.isEnabled && enabled && this.port !== port; | ||
|
|
||
| if (!enabled && this.isEnabled) { | ||
| this.stop(); | ||
| } else if (enabled && !this.isEnabled) { | ||
| this.start(port); | ||
| } else if (needsRestart) { | ||
| this.stop(); | ||
| this.start(port); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Register a handler for remote control API endpoints | ||
| */ | ||
| registerRemoteControlHandler( | ||
| path: string, | ||
| handler: (req: http.IncomingMessage, res: http.ServerResponse) => void | ||
| ): void { | ||
| this.remoteControlHandlers.set(path, handler); | ||
| } | ||
|
|
||
| /** | ||
| * Handle incoming HTTP requests | ||
| */ | ||
| private handleRequest( | ||
| req: http.IncomingMessage, | ||
| res: http.ServerResponse | ||
| ): void { | ||
| const url = req.url || '/'; | ||
|
|
||
| // Handle API requests | ||
| if (url.startsWith('/api/remote-control/')) { | ||
| const handler = this.remoteControlHandlers.get(url); | ||
| if (handler) { | ||
| handler(req, res); | ||
| return; | ||
| } | ||
|
|
||
| res.writeHead(404, { 'Content-Type': 'application/json' }); | ||
| res.end(JSON.stringify({ error: 'Endpoint not found' })); | ||
| return; | ||
| } | ||
|
|
||
| // Serve static files from the remote-control-web app | ||
| this.serveStaticFile(url, res); | ||
| } | ||
|
|
||
| /** | ||
| * Serve static files | ||
| */ | ||
| private serveStaticFile(url: string, res: http.ServerResponse): void { | ||
| // Default to index.html for root path | ||
| let filePath = url === '/' ? '/index.html' : url; | ||
|
|
||
| // Security: prevent directory traversal | ||
| filePath = path.normalize(filePath).replace(/^(\.\.[/\\])+/, ''); | ||
|
|
||
| const fullPath = path.join(this.distPath, filePath); | ||
|
|
||
| fs.readFile(fullPath, (err, data) => { | ||
| if (err) { | ||
| // If file not found, try serving index.html (for Angular routing) | ||
| if (err.code === 'ENOENT' && filePath !== '/index.html') { | ||
| this.serveStaticFile('/', res); | ||
| return; | ||
| } | ||
|
|
||
| res.writeHead(404, { 'Content-Type': 'text/plain' }); | ||
| res.end('404 Not Found'); | ||
| return; | ||
| } | ||
|
|
||
| // Determine content type | ||
| const contentType = this.getContentType(fullPath); | ||
| res.writeHead(200, { 'Content-Type': contentType }); | ||
| res.end(data); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Get content type based on file extension | ||
| */ | ||
| private getContentType(filePath: string): string { | ||
| const ext = path.extname(filePath).toLowerCase(); | ||
| const contentTypes: Record<string, string> = { | ||
| '.html': 'text/html', | ||
| '.js': 'application/javascript', | ||
| '.css': 'text/css', | ||
| '.json': 'application/json', | ||
| '.png': 'image/png', | ||
| '.jpg': 'image/jpeg', | ||
| '.gif': 'image/gif', | ||
| '.svg': 'image/svg+xml', | ||
| '.ico': 'image/x-icon', | ||
| '.woff': 'font/woff', | ||
| '.woff2': 'font/woff2', | ||
| '.ttf': 'font/ttf', | ||
| }; | ||
|
|
||
| return contentTypes[ext] || 'application/octet-stream'; | ||
| } | ||
| } | ||
|
|
||
| export const httpServer = new HttpServer(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add cleanup mechanism for the IPC listener.
The
onChannelChangelistener registration doesn't provide a way to unsubscribe, which can lead to memory leaks if multiple listeners are registered or if the component using this API is destroyed and recreated.Consider returning a cleanup function:
🤖 Prompt for AI Agents