preview_media_page.dart 8.78 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 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).padding.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!),
      ),
    );
  }
}