import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:video_player/video_player.dart'; import 'package:open_filex/open_filex.dart'; import 'package:flutter/gestures.dart'; class MediaItem { final String path; final bool isVideo; MediaItem({required this.path, required this.isVideo}); } class MediaViewer extends StatefulWidget { final List items; final int initialIndex; const MediaViewer({super.key, required this.items, this.initialIndex = 0}); @override State createState() => _MediaViewerState(); } class _MediaViewerState extends State { late PageController _pageController; VideoPlayerController? _videoController; String? _videoInitError; final FocusNode _focusNode = FocusNode(); int _index = 0; bool _uiVisible = true; bool _isLandscape = false; @override void initState() { super.initState(); _index = widget.initialIndex; _pageController = PageController(initialPage: _index); print("MediaViewer: Initial index set to $_index"); print("MediaViewer: Total items = ${widget.items.length}"); print( "MediaViewer: Initial media item = ${widget.items[_index].path}, isVideo = ${widget.items[_index].isVideo}", ); _hideSystemUI(); _initVideoIfNeeded(_index); _focusNode.requestFocus(); } void _handleKeyEvent(KeyEvent event) { if (event is! KeyDownEvent) return; final item = widget.items[_index]; if (!item.isVideo || _videoController == null || !_videoController!.value.isInitialized) return; final c = _videoController!; if (event.logicalKey == LogicalKeyboardKey.space) { setState(() { c.value.isPlaying ? c.pause() : c.play(); }); } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { final currentPos = c.value.position; final newPos = currentPos - const Duration(seconds: 5); c.seekTo(newPos < Duration.zero ? Duration.zero : newPos); } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { final currentPos = c.value.position; final maxDur = c.value.duration; final newPos = currentPos + const Duration(seconds: 5); c.seekTo(newPos > maxDur ? maxDur : newPos); } } Future _initVideoIfNeeded(int index) async { _videoController?.removeListener(_videoListener); _videoController?.dispose(); _videoController = null; _videoInitError = null; final item = widget.items[index]; if (!item.isVideo) return; final controller = VideoPlayerController.file(File(item.path)); _videoController = controller; try { await controller.initialize(); _videoController!.addListener(_videoListener); controller.setLooping(false); controller.play(); _videoInitError = null; } catch (e) { _videoInitError = e.toString(); _videoController?.removeListener(_videoListener); await _videoController?.dispose().catchError((_) {}); _videoController = null; } finally { if (mounted) setState(() {}); } } void _videoListener() { if (mounted) { setState(() {}); } } void _hideSystemUI() { if (Platform.isAndroid || Platform.isIOS) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); } } void _showSystemUI() { if (Platform.isAndroid || Platform.isIOS) { SystemChrome.setEnabledSystemUIMode( SystemUiMode.manual, overlays: SystemUiOverlay.values, ); } } void _toggleOrientation() { setState(() { _isLandscape = !_isLandscape; }); if (_isLandscape) { SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, DeviceOrientation.landscapeRight, ]); } else { SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); } } @override void dispose() { _videoController?.removeListener(_videoListener); _videoController?.dispose(); _pageController.dispose(); _showSystemUI(); SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); super.dispose(); } void _toggleUI() { setState(() => _uiVisible = !_uiVisible); } String _format(Duration d) { String two(int n) => n.toString().padLeft(2, '0'); return "${two(d.inMinutes.remainder(60))}:${two(d.inSeconds.remainder(60))}"; } @override Widget build(BuildContext context) { final isVideoReady = widget.items[_index].isVideo && _videoController != null && _videoController!.value.isInitialized; return Scaffold( backgroundColor: Colors.black, body: KeyboardListener( focusNode: _focusNode, autofocus: true, onKeyEvent: _handleKeyEvent, child: SafeArea( child: GestureDetector( onTap: _toggleUI, onLongPress: () { if (isVideoReady) { final currentSpeed = _videoController!.value.playbackSpeed; _videoController!.setPlaybackSpeed( currentSpeed == 1.0 ? 2.0 : 1.0, ); } }, behavior: HitTestBehavior.opaque, child: Stack( children: [ // MEDIA PAGES Positioned.fill( child: ScrollConfiguration( behavior: ScrollConfiguration.of(context).copyWith( dragDevices: { PointerDeviceKind.touch, PointerDeviceKind.mouse, PointerDeviceKind.trackpad, }, ), child: PageView.builder( controller: _pageController, onPageChanged: (i) async { setState(() => _index = i); await _initVideoIfNeeded(i); }, itemCount: widget.items.length, itemBuilder: (context, i) { final item = widget.items[i]; if (item.isVideo) { if (_videoInitError != null) { return _buildVideoInitErrorFallback(item.path); } if (_videoController == null || !_videoController!.value.isInitialized) { return const Center( child: CircularProgressIndicator( color: Colors.white, ), ); } return Center( child: AspectRatio( aspectRatio: _videoController!.value.aspectRatio, child: VideoPlayer(_videoController!), ), ); } return Center( child: InteractiveViewer( maxScale: 4.0, child: Image.file( File(item.path), fit: BoxFit.contain, ), ), ); }, ), ), ), // ЦЕНТРАЛЬНАЯ КНОПКА ПЛЕЙ/ПАУЗА if (_uiVisible && isVideoReady) Center( child: Container( decoration: const BoxDecoration( color: Colors.black38, shape: BoxShape.circle, ), child: IconButton( iconSize: 64, icon: Icon( _videoController!.value.isPlaying ? Icons.pause : Icons.play_arrow, color: Colors.white, ), onPressed: () { setState(() { _videoController!.value.isPlaying ? _videoController!.pause() : _videoController!.play(); }); }, ), ), ), // TOP BAR if (_uiVisible) Positioned( top: 10, left: 16, right: 16, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( icon: const Icon( Icons.close, color: Colors.white, size: 28, ), onPressed: () => Navigator.pop(context), ), IconButton( icon: const Icon( Icons.screen_rotation, color: Colors.white, size: 26, ), onPressed: _toggleOrientation, ), ], ), ), // VIDEO CONTROLS if (_uiVisible && isVideoReady) Positioned( bottom: 10, left: 16, right: 16, child: _buildVideoControls(), ), ], ), ), ), ), ); } Widget _buildVideoControls() { final c = _videoController!; final pos = c.value.position; final dur = c.value.duration; final posMs = pos.inMilliseconds.toDouble(); final maxMs = dur.inMilliseconds .toDouble() .clamp(1, double.infinity) .toDouble(); return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: Colors.black54, borderRadius: BorderRadius.circular(30), ), child: Row( children: [ IconButton( icon: Icon( c.value.isPlaying ? Icons.pause : Icons.play_arrow, color: Colors.white, size: 28, ), onPressed: () { setState(() { c.value.isPlaying ? c.pause() : c.play(); }); }, ), Expanded( child: SliderTheme( data: SliderTheme.of(context).copyWith( trackHeight: 4, activeTrackColor: Colors.white60, inactiveTrackColor: Colors.white30, thumbColor: Colors.white, thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6), overlayColor: Colors.transparent, ), child: Slider( value: posMs.clamp(0, maxMs), min: 0, max: maxMs, onChanged: (v) { c.seekTo(Duration(milliseconds: v.toInt())); }, ), ), ), const SizedBox(width: 8), Text( "${_format(pos)} / ${_format(dur)}", style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold, ), ), const SizedBox(width: 8), ], ), ); } Widget _buildVideoInitErrorFallback(String path) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.play_disabled, color: Colors.white70, size: 56), const SizedBox(height: 10), const Text( 'Видео не воспроизводится на этом устройстве', textAlign: TextAlign.center, style: TextStyle(color: Colors.white70), ), const SizedBox(height: 10), OutlinedButton.icon( onPressed: () async { try { await OpenFilex.open(path); } catch (_) {} }, icon: const Icon(Icons.open_in_new, color: Colors.white70), label: const Text( 'Открыть внешним плеером', style: TextStyle(color: Colors.white70), ), ), ], ), ); } }