// lib/widgets/viewer/fullscreen_cast_viewer.dart // // Viewer FULLSCREEN per Google Cast. // FAST e DLNA hanno viewer separati. import 'dart:io'; import 'package:flutter/material.dart'; import 'package:video_player/video_player.dart'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/widgets/cast/cast_controller.dart'; class FullscreenCastViewer extends StatefulWidget { final List entries; final int startIndex; const FullscreenCastViewer({ super.key, required this.entries, required this.startIndex, }); @override State createState() => _FullscreenCastViewerState(); } class _FullscreenCastViewerState extends State { late final PageController _pageController; late int _currentIndex; bool _isUpdatingFromCast = false; bool _controlsVisible = true; @override void initState() { super.initState(); _currentIndex = widget.startIndex.clamp(0, widget.entries.length - 1); _pageController = PageController(initialPage: _currentIndex); CastController.instance.currentIndexNotifier.addListener(_onCastIndexChanged); } @override void dispose() { CastController.instance.currentIndexNotifier.removeListener(_onCastIndexChanged); _pageController.dispose(); super.dispose(); } // ------------------------------------------------------------ // SYNC CAST -> UI // ------------------------------------------------------------ void _onCastIndexChanged() { final newIndex = CastController.instance.currentIndexNotifier.value; if (newIndex == _currentIndex) return; _isUpdatingFromCast = true; _currentIndex = newIndex; if (_pageController.hasClients) { _pageController.jumpToPage(newIndex); } if (mounted) setState(() {}); Future.delayed(const Duration(milliseconds: 100), () { _isUpdatingFromCast = false; }); } // ------------------------------------------------------------ // SYNC UI -> CAST // ------------------------------------------------------------ void _onPageChanged(int index) { if (_isUpdatingFromCast) return; if (index > _currentIndex) { CastController.instance.nextManual(); } else if (index < _currentIndex) { CastController.instance.prevManual(); } setState(() => _currentIndex = index); } // ------------------------------------------------------------ // Helpers UI // ------------------------------------------------------------ String _formatDuration(Duration d) { final totalSeconds = d.inSeconds; final hours = totalSeconds ~/ 3600; final minutes = (totalSeconds % 3600) ~/ 60; final seconds = totalSeconds % 60; if (hours > 0) { return '${hours.toString().padLeft(2, '0')}:' '${minutes.toString().padLeft(2, '0')}:' '${seconds.toString().padLeft(2, '0')}'; } return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; } Widget _buildTopBar(BuildContext context) { return SafeArea( child: Container( color: Colors.black54, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), child: Row( children: [ IconButton( icon: const Icon(Icons.close, color: Colors.white), onPressed: () => Navigator.of(context).maybePop(), ), Expanded( child: Text( '${_currentIndex + 1}/${widget.entries.length}', style: const TextStyle(color: Colors.white), textAlign: TextAlign.center, ), ), IconButton( icon: const Icon(Icons.stop, color: Colors.white), onPressed: () async { await CastController.instance.stopCast(); if (mounted) Navigator.of(context).maybePop(); }, ), ], ), ), ); } Widget _buildBottomControls() { final controller = CastController.instance; return SafeArea( top: false, child: Container( color: Colors.black54, padding: const EdgeInsets.fromLTRB(12, 8, 12, 12), child: Column( mainAxisSize: MainAxisSize.min, children: [ // -------------------------------------------------- // SEEK BAR (solo Google) // -------------------------------------------------- ValueListenableBuilder( valueListenable: controller.backendNotifier, builder: (context, backend, _) { if (backend != CastBackend.google) return const SizedBox.shrink(); return ValueListenableBuilder( valueListenable: controller.durationNotifier, builder: (context, duration, _) { return ValueListenableBuilder( valueListenable: controller.positionNotifier, builder: (context, position, __) { final maxMs = duration.inMilliseconds <= 0 ? 1 : duration.inMilliseconds; final valueMs = position.inMilliseconds.clamp(0, maxMs); return Column( children: [ Slider( value: valueMs.toDouble(), min: 0, max: maxMs.toDouble(), onChanged: (_) {}, onChangeEnd: (_) {}, ), Padding( padding: const EdgeInsets.symmetric(horizontal: 8), child: Row( children: [ Text(_formatDuration(position), style: const TextStyle(color: Colors.white70)), const Spacer(), Text(_formatDuration(duration), style: const TextStyle(color: Colors.white70)), ], ), ), ], ); }, ); }, ); }, ), const SizedBox(height: 8), // -------------------------------------------------- // PREV / PLAY-PAUSE / NEXT // -------------------------------------------------- Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( iconSize: 34, icon: const Icon(Icons.skip_previous, color: Colors.white), onPressed: () => controller.prevManual(), ), const SizedBox(width: 12), // PLAY/PAUSE solo Google ValueListenableBuilder( valueListenable: controller.backendNotifier, builder: (context, backend, _) { if (backend != CastBackend.google) return const SizedBox.shrink(); return ValueListenableBuilder( valueListenable: controller.isPlayingNotifier, builder: (context, isPlaying, _) { return IconButton( iconSize: 40, icon: Icon( isPlaying ? Icons.pause_circle : Icons.play_circle, color: Colors.white, ), onPressed: () {}, ); }, ); }, ), const SizedBox(width: 12), IconButton( iconSize: 34, icon: const Icon(Icons.skip_next, color: Colors.white), onPressed: () => controller.nextManual(), ), ], ), const SizedBox(height: 12), // -------------------------------------------------- // VOLUME solo Google // -------------------------------------------------- ValueListenableBuilder( valueListenable: controller.backendNotifier, builder: (context, backend, _) { if (backend != CastBackend.google) return const SizedBox.shrink(); return ValueListenableBuilder( valueListenable: controller.volumeNotifier, builder: (context, volume, _) { return Row( children: [ IconButton( icon: const Icon(Icons.volume_up, color: Colors.white), onPressed: () {}, ), Expanded( child: Slider( value: volume.clamp(0.0, 1.0), min: 0, max: 1, onChanged: (_) {}, ), ), ], ); }, ); }, ), ], ), ), ); } // ------------------------------------------------------------ // RENDER ENTRY (locale o remoto) // ------------------------------------------------------------ Widget _buildEntry(AvesEntry entry) { final isVideo = (entry.durationMillis ?? 0) > 0; final path = entry.path; // VIDEO LOCALE if (isVideo && path != null && File(path).existsSync()) { return Center(child: VideoWidget.file(File(path))); } // FOTO LOCALE if (!isVideo && path != null && File(path).existsSync()) { return InteractiveViewer( child: Center(child: Image.file(File(path), fit: BoxFit.contain)), ); } return const Center( child: Icon(Icons.broken_image, size: 64, color: Colors.white54), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => setState(() => _controlsVisible = !_controlsVisible), child: Stack( children: [ PageView.builder( controller: _pageController, itemCount: widget.entries.length, onPageChanged: _onPageChanged, itemBuilder: (context, i) => _buildEntry(widget.entries[i]), ), if (_controlsVisible) ...[ Align(alignment: Alignment.topCenter, child: _buildTopBar(context)), Align(alignment: Alignment.bottomCenter, child: _buildBottomControls()), ], ], ), ), ); } } // ------------------------------------------------------------ // VIDEO WIDGET (solo locale) // ------------------------------------------------------------ class VideoWidget extends StatefulWidget { final File file; const VideoWidget.file(this.file, {super.key}); @override State createState() => _VideoWidgetState(); } class _VideoWidgetState extends State { VideoPlayerController? controller; @override void initState() { super.initState(); controller = VideoPlayerController.file(widget.file) ..initialize().then((_) { if (mounted) setState(() {}); }); } @override void dispose() { controller?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (controller == null || !controller!.value.isInitialized) { return const Center(child: CircularProgressIndicator()); } return AspectRatio( aspectRatio: controller!.value.aspectRatio, child: VideoPlayer(controller!), ); } }