| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251 |
- 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<RemoteScreen> createState() => _RemoteScreenState();
- }
- class _RemoteScreenState extends State<RemoteScreen> {
- 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<String, dynamic>;
- _handleMessage(msg);
- },
- onError: (e) {
- setState(() {
- _connected = false;
- _error = 'Connection error: $e';
- });
- },
- onDone: () {
- setState(() {
- _connected = false;
- _error = 'Disconnected from server.';
- });
- },
- );
- }
- void _handleMessage(Map<String, dynamic> 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)),
- ],
- ),
- ),
- ),
- );
- }
- }
|