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 createState() => _VideoZonePageState(); } class _VideoZonePageState extends State { late Future> _itemsFuture; @override void initState() { super.initState(); _itemsFuture = _loadItems(); } Future> _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>( 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 []; 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 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, ), ], ), ); } }