import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; import 'package:flutter/material.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; // ─── Remote Screen ──────────────────────────────────────────────────────────── class RemoteScreen extends StatefulWidget { final String host; final int port; const RemoteScreen({super.key, required this.host, required this.port}); @override State createState() => _RemoteScreenState(); } class _RemoteScreenState extends State { late WebSocketChannel _channel; StreamSubscription? _sub; int _current = 0; int _total = 0; Uint8List? _slideImage; bool _connected = false; String? _error; @override void initState() { super.initState(); _connect(); } void _connect() { final uri = Uri.parse('ws://${widget.host}:${widget.port}'); _channel = WebSocketChannel.connect(uri); setState(() { _connected = true; _error = null; }); _sub = _channel.stream.listen( (data) { final msg = jsonDecode(data as String) as Map; _handleMessage(msg); }, onError: (e) { setState(() { _connected = false; _error = 'Connection error: $e'; }); }, onDone: () { setState(() { _connected = false; _error = 'Disconnected from server.'; }); }, ); } void _handleMessage(Map msg) { final event = msg['event'] as String?; if (event == 'slide') { setState(() { _current = (msg['current'] as num).toInt(); _total = (msg['total'] as num).toInt(); }); } else if (event == 'image') { final b64 = msg['data'] as String?; if (b64 != null && b64.isNotEmpty) { setState(() => _slideImage = base64Decode(b64)); } } else if (event == 'error') { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(msg['message'] ?? 'Unknown error')), ); } } void _send(String cmd) { if (!_connected) return; _channel.sink.add(jsonEncode({'cmd': cmd})); } @override void dispose() { _sub?.cancel(); _channel.sink.close(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text('PPT Remote — ${widget.host}'), actions: [ Icon( _connected ? Icons.wifi : Icons.wifi_off, color: _connected ? Colors.greenAccent : Colors.redAccent, ), const SizedBox(width: 12), ], ), body: Column( children: [ // Slide preview Expanded( child: _slideImage != null ? Padding( padding: const EdgeInsets.all(16), child: ClipRRect( borderRadius: BorderRadius.circular(12), child: Image.memory( _slideImage!, fit: BoxFit.contain, ), ), ) : Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.slideshow, size: 80, color: Colors.white24), const SizedBox(height: 12), Text( _error ?? 'No slide preview', style: const TextStyle(color: Colors.white38), textAlign: TextAlign.center, ), ], ), ), ), // Slide counter if (_total > 0) Text( 'Slide $_current / $_total', style: Theme.of(context).textTheme.headlineSmall, ), // Progress bar if (_total > 0) Padding( padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8), child: LinearProgressIndicator( value: _current / _total, minHeight: 6, borderRadius: BorderRadius.circular(4), ), ), // Controls Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 32), child: Column( children: [ // Navigation row Row( children: [ Expanded( child: _NavButton( icon: Icons.arrow_back_ios_new, label: 'Prev', onTap: () => _send('prev'), ), ), const SizedBox(width: 12), Expanded( child: _NavButton( icon: Icons.arrow_forward_ios, label: 'Next', onTap: () => _send('next'), ), ), ], ), const SizedBox(height: 12), // Slideshow controls row Row( children: [ Expanded( child: _NavButton( icon: Icons.play_arrow, label: 'Start Show', color: Colors.green, onTap: () => _send('start'), ), ), const SizedBox(width: 12), Expanded( child: _NavButton( icon: Icons.stop, label: 'End Show', color: Colors.red, onTap: () => _send('end'), ), ), ], ), ], ), ), ], ), ); } } // ─── Nav Button ─────────────────────────────────────────────────────────────── class _NavButton extends StatelessWidget { final IconData icon; final String label; final VoidCallback onTap; final Color? color; const _NavButton({ required this.icon, required this.label, required this.onTap, this.color, }); @override Widget build(BuildContext context) { return Material( color: (color ?? Theme.of(context).colorScheme.primary).withOpacity(0.15), borderRadius: BorderRadius.circular(16), child: InkWell( borderRadius: BorderRadius.circular(16), onTap: onTap, child: Padding( padding: const EdgeInsets.symmetric(vertical: 20), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 32, color: color ?? Theme.of(context).colorScheme.primary), const SizedBox(height: 6), Text(label, style: TextStyle(color: color ?? Theme.of(context).colorScheme.primary)), ], ), ), ), ); } }