preview_media_page.dart 9.01 KB
import 'package:appframe/data/models/media_item.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart';
import 'package:video_player/video_player.dart';

/// 媒体预览页面
///
/// 使用 PhotoViewGallery.builder 配合 photo_view(图片)和 chewie(视频)实现
/// 图片缩放查看与视频播放,支持左右滑动切换。
/// PhotoViewGallery 内部已包含 PhotoViewGestureDetectorScope,可自动协调
/// PageView 与 PhotoView 的手势竞技,无需手动包裹。
class PreviewMediaPage extends StatefulWidget {
  const PreviewMediaPage({super.key});

  @override
  State<PreviewMediaPage> createState() => _PreviewMediaPageState();
}

class _PreviewMediaPageState extends State<PreviewMediaPage> {
  PageController? _pageController;
  int _currentIndex = 0;
  List<MediaItem> _items = const [];
  bool _initialized = false;

  /// 视频条目的 GlobalKey,用于翻页时通知暂停其它视频
  final Map<int, GlobalKey<_VideoItemState>> _videoKeys = {};

  @override
  Widget build(BuildContext context) {
    if (!_initialized) {
      final Map<String, dynamic>? extra =
          GoRouterState.of(context).extra as Map<String, dynamic>?;
      final List<MediaItem> items =
          ((extra?['items'] as List?)?.cast<MediaItem>()) ?? const [];
      _items = List<MediaItem>.unmodifiable(items);
      _currentIndex = (extra?['current'] as int?) ?? 0;
      if (_currentIndex < 0 || _currentIndex >= _items.length) {
        _currentIndex = 0;
      }
      _pageController = PageController(initialPage: _currentIndex);
      _initialized = true;
    }

    return ColoredBox(
      color: Colors.black,
      child: Scaffold(
        backgroundColor: Colors.black,
        body: Stack(
          children: [
            Positioned.fill(
              child: PhotoViewGallery.builder(
                pageController: _pageController,
                itemCount: _items.length,
                onPageChanged: _onPageChanged,
                backgroundDecoration: const BoxDecoration(color: Colors.black),
                loadingBuilder: (ctx, event) => const Center(
                  child: CircularProgressIndicator(color: Colors.white),
                ),
                builder: (ctx, index) {
                  final MediaItem item = _items[index];
                  final String heroTag = 'preview_media_${index}_${item.url}';
                  if (item.type == MediaType.video) {
                    final GlobalKey<_VideoItemState> key = _videoKeys
                        .putIfAbsent(index, () => GlobalKey<_VideoItemState>());
                    // 视频页:使用 customChild 嵌入自定义视频组件
                    // disableGestures=true 关闭 PhotoView 的缩放手势,
                    // 把控制权完全交给 Chewie 自身的进度条/暂停等操作。
                    return PhotoViewGalleryPageOptions.customChild(
                      disableGestures: true,
                      heroAttributes: PhotoViewHeroAttributes(tag: heroTag),
                      child: _VideoItem(
                        key: key,
                        url: item.url,
                        poster: item.poster,
                        autoPlay: index == _currentIndex,
                      ),
                    );
                  }
                  // 图片页:标准 PhotoView 配置
                  return PhotoViewGalleryPageOptions(
                    imageProvider: CachedNetworkImageProvider(item.url),
                    minScale: PhotoViewComputedScale.contained,
                    maxScale: PhotoViewComputedScale.covered * 3,
                    initialScale: PhotoViewComputedScale.contained,
                    heroAttributes: PhotoViewHeroAttributes(tag: heroTag),
                    onTapUp: (_, __, ___) => _close(),
                    errorBuilder: (c, e, s) => const Center(
                      child: Icon(
                        Icons.broken_image,
                        color: Colors.white54,
                        size: 48,
                      ),
                    ),
                  );
                },
              ),
            ),
            Positioned(
              top: MediaQuery.of(context).padding.top + 8,
              right: 8,
              child: IconButton(
                icon: const Icon(Icons.close, color: Colors.white, size: 28),
                onPressed: _close,
              ),
            ),
            if (_items.length > 1)
              Positioned(
                bottom: MediaQuery.of(context).viewPadding.bottom + 24,
                left: 0,
                right: 0,
                child: Center(
                  child: Container(
                    padding:
                        const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
                    decoration: BoxDecoration(
                      color: Colors.black54,
                      borderRadius: BorderRadius.circular(12),
                    ),
                    child: Text(
                      '${_currentIndex + 1} / ${_items.length}',
                      style: const TextStyle(
                        color: Colors.white,
                        fontSize: 14,
                      ),
                    ),
                  ),
                ),
              ),
          ],
        ),
      ),
    );
  }

  void _onPageChanged(int index) {
    // 通知所有非当前页的视频暂停,避免声音重叠
    _videoKeys.forEach((i, key) {
      if (i != index) {
        key.currentState?.pause();
      }
    });
    setState(() {
      _currentIndex = index;
    });
  }

  void _close() {
    if (context.canPop()) {
      context.pop();
    }
  }

  @override
  void dispose() {
    _pageController?.dispose();
    super.dispose();
  }
}

/// 视频项:VideoPlayer + Chewie
///
/// 必须为 StatefulWidget,由 [initState] 初始化控制器,[dispose] 销毁。
/// Hero 动画由 PhotoViewGalleryPageOptions.customChild 的 heroAttributes 接管,
/// 此处不再额外包 Hero,避免双重包裹冲突。
class _VideoItem extends StatefulWidget {
  final String url;
  final String? poster;
  final bool autoPlay;

  const _VideoItem({
    super.key,
    required this.url,
    this.poster,
    this.autoPlay = false,
  });

  @override
  State<_VideoItem> createState() => _VideoItemState();
}

class _VideoItemState extends State<_VideoItem> {
  VideoPlayerController? _videoController;
  ChewieController? _chewieController;
  bool _initializing = true;
  Object? _error;

  @override
  void initState() {
    super.initState();
    _initController();
  }

  Future<void> _initController() async {
    try {
      final VideoPlayerController controller =
          VideoPlayerController.networkUrl(Uri.parse(widget.url));
      _videoController = controller;
      await controller.initialize();
      if (!mounted) {
        await controller.dispose();
        return;
      }
      _chewieController = ChewieController(
        videoPlayerController: controller,
        autoPlay: widget.autoPlay,
        looping: false,
        allowFullScreen: true,
        allowMuting: true,
        showControls: true,
        aspectRatio: controller.value.aspectRatio,
        materialProgressColors: ChewieProgressColors(
          playedColor: Colors.white,
          handleColor: Colors.white,
          bufferedColor: Colors.white38,
          backgroundColor: Colors.white12,
        ),
        placeholder: _buildPosterPlaceholder(),
      );
      setState(() {
        _initializing = false;
      });
    } catch (e) {
      if (!mounted) {
        return;
      }
      setState(() {
        _error = e;
        _initializing = false;
      });
    }
  }

  /// 暂停视频(被外部翻页时调用)
  void pause() {
    _videoController?.pause();
  }

  /// 视频占位图:有 poster 时使用封面图,否则黑色底
  Widget _buildPosterPlaceholder() {
    final String? poster = widget.poster;
    if (poster == null || poster.isEmpty) {
      return const ColoredBox(color: Colors.black);
    }
    return Center(
      child: CachedNetworkImage(
        imageUrl: poster,
        fit: BoxFit.contain,
        errorWidget: (c, u, e) => const ColoredBox(color: Colors.black),
      ),
    );
  }

  @override
  void dispose() {
    _chewieController?.dispose();
    _videoController?.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (_initializing) {
      return const Center(
        child: CircularProgressIndicator(color: Colors.white),
      );
    }
    if (_error != null || _chewieController == null) {
      return const Center(
        child: Icon(
          Icons.error_outline,
          color: Colors.white54,
          size: 48,
        ),
      );
    }
    return Center(
      child: AspectRatio(
        aspectRatio: _videoController!.value.aspectRatio,
        child: Chewie(controller: _chewieController!),
      ),
    );
  }
}