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'; 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; int _index = 0; bool _uiVisible = true; bool _isLandscape = false; @override void initState() { super.initState(); _index = widget.initialIndex; _pageController = PageController(initialPage: _index); // 1. Скрываем строку состояния и панель навигации при входе в плеер _hideSystemUI(); _initVideoIfNeeded(_index); } 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(() {}); } } // Метод скрытия системного UI (Status bar и Navigation bar) void _hideSystemUI() { SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); } // Метод показа системного UI при выходе из полноэкранного режима void _showSystemUI() { 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(); // 2. Обязательно возвращаем системный UI и портретный режим при выходе _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) { return Scaffold( backgroundColor: Colors.black, // SafeArea гарантирует, что даже если системные бары скрыты/показаны, // интерактивные элементы интерфейса (кнопки закрытия, плеер) // никогда не уйдут под физические вырезы экрана (челку, скругления) body: SafeArea( child: GestureDetector( onTap: _toggleUI, behavior: HitTestBehavior.opaque, child: Stack( children: [ // MEDIA PAGES (Контент растягивается на весь экран) Positioned.fill( 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, ), ), ); }, ), ), // TOP BAR (Кнопки управления сверху) if (_uiVisible) Positioned( top: 10, // Маленький фиксированный отступ, т.к. SafeArea уже защищает сверху 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: Icon( Icons.screen_rotation, color: Colors.white, size: 26, ), onPressed: _toggleOrientation, ), ], ), ), // VIDEO CONTROLS (Нижняя панель управления видео) if (_uiVisible && widget.items[_index].isVideo && _videoController != null && _videoController!.value.isInitialized) Positioned( bottom: 10, // Прижато к низу безопасной зоны SafeArea 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), ), ), ], ), ); } }