video_zone_page.dart 6.45 KB
import 'dart:async';

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:wow_english/common/dialogs/show_dialog.dart';
import 'package:wow_english/common/request/dao/video_dao.dart';
import 'package:wow_english/common/widgets/we_app_bar.dart';
import 'package:wow_english/route/route.dart';
import 'package:wow_english/utils/toast_util.dart';

import 'video_zone_item.dart';

class VideoZonePage extends StatefulWidget {
  const VideoZonePage({super.key});

  @override
  State<VideoZonePage> createState() => _VideoZonePageState();
}

class _VideoZonePageState extends State<VideoZonePage> {
  late Future<List<VideoZoneItem>> _itemsFuture;

  @override
  void initState() {
    super.initState();
    _itemsFuture = _loadItems();
  }

  Future<List<VideoZoneItem>> _loadItems() async {
    try {
      return await VideoDao.list().timeout(const Duration(seconds: 10));
    } on TimeoutException catch (error) {
      debugPrint('获取视频列表超时: $error');
      showToast('视频列表加载超时,请检查网络后重试');
      rethrow;
    } catch (error) {
      debugPrint('获取视频列表失败: $error');
      showToast('视频列表加载失败,请稍后重试');
      rethrow;
    }
  }

  void _reload() {
    setState(() {
      _itemsFuture = _loadItems();
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.white,
      appBar: const WEAppBar(
        titleText: '视频专区',
        centerTitle: false,
      ),
      body: FutureBuilder<List<VideoZoneItem>>(
        future: _itemsFuture,
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasError) {
            return _VideoLoadError(onRetry: _reload);
          }
          final items = snapshot.data ?? const <VideoZoneItem>[];
          if (items.isEmpty) {
            return const Center(
              child: Text(
                '暂无视频',
                style: TextStyle(color: Color(0xFF999999), fontSize: 18),
              ),
            );
          }
          return RefreshIndicator(
            onRefresh: () async {
              final future = _loadItems();
              setState(() => _itemsFuture = future);
              try {
                await future;
              } catch (_) {
                // The FutureBuilder shows the error state; _loadItems already toasts it.
              }
            },
            child: LayoutBuilder(
              builder: (context, constraints) {
                final titleStyle = _VideoCard.createTitleStyle(context);
                return GridView.builder(
                  physics: const AlwaysScrollableScrollPhysics(),
                  padding: EdgeInsets.fromLTRB(
                    14.w,
                    10.h,
                    14.w,
                    16.h,
                  ),
                  gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 4,
                    mainAxisSpacing: 14.h,
                    crossAxisSpacing: 12.w,
                    childAspectRatio: 1.55,
                  ),
                  itemCount: items.length,
                  itemBuilder: (context, index) => _VideoCard(
                    item: items[index],
                    onTap: () => _openVideo(items, index),
                    titleStyle: titleStyle,
                  ),
                );
              },
            ),
          );
        },
      ),
    );
  }

  void _openVideo(List<VideoZoneItem> items, int index) {
    final item = items[index];
    if (!item.accessible || item.videoUrl.isEmpty) {
      showTwoActionDialog('提示', '取消', '去购买', '购买课程后即可观看视频。', leftTap: popPage,
          rightTap: () {
        popPage();
        pushNamed(AppRouteName.shop);
      });
      return;
    }
    Navigator.of(context).pushNamed(
      AppRouteName.videoZonePlayer,
      arguments: {
        'items': items
            .where((video) => video.accessible && video.videoUrl.isNotEmpty)
            .toList(),
        'initialVideoId': item.id,
      },
    ).then((_) => _reload());
  }
}

class _VideoLoadError extends StatelessWidget {
  const _VideoLoadError({required this.onRetry});

  final VoidCallback onRetry;

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          const Text(
            '视频加载失败,请稍后重试',
            style: TextStyle(color: Color(0xFF999999), fontSize: 18),
          ),
          const SizedBox(height: 12),
          FilledButton(onPressed: onRetry, child: const Text('重新加载')),
        ],
      ),
    );
  }
}

class _VideoCard extends StatelessWidget {
  const _VideoCard({
    required this.item,
    required this.onTap,
    required this.titleStyle,
  });

  final VideoZoneItem item;
  final VoidCallback onTap;
  final TextStyle titleStyle;

  static TextStyle createTitleStyle(BuildContext context) => TextStyle(
        color: const Color(0xFF222222),
        fontFamily: null,
        fontSize: 17.sp,
        fontWeight: FontWeight.w500,
      );

  @override
  Widget build(BuildContext context) {
    return InkWell(
      borderRadius: BorderRadius.circular(12.r),
      onTap: onTap,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Expanded(
            child: ClipRRect(
              borderRadius: BorderRadius.circular(12.r),
              child: CachedNetworkImage(
                imageUrl: item.coverUrl,
                fit: BoxFit.cover,
                placeholder: (_, __) => const ColoredBox(
                  color: Color(0xFFF2F2F2),
                  child: Center(child: CircularProgressIndicator()),
                ),
                errorWidget: (_, __, ___) => const ColoredBox(
                  color: Color(0xFFF2F2F2),
                  child: Icon(Icons.broken_image_outlined,
                      color: Color(0xFFAAAAAA)),
                ),
              ),
            ),
          ),
          SizedBox(height: 5.h),
          Text(
            item.title,
            maxLines: 1,
            overflow: TextOverflow.ellipsis,
            style: titleStyle,
          ),
        ],
      ),
    );
  }
}