Chepuhagram/lib/presentation/screens/media_viewer_screen.dart

331 lines
10 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<MediaItem> items;
final int initialIndex;
const MediaViewer({
super.key,
required this.items,
this.initialIndex = 0,
});
@override
State<MediaViewer> createState() => _MediaViewerState();
}
class _MediaViewerState extends State<MediaViewer> {
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<void> _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),
),
),
],
),
);
}
}