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

import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.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 '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();
    unawaited(
      SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky),
    );
    _itemsFuture = VideoDao.list();
  }

  @override
  void dispose() {
    unawaited(
      SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky),
    );
    super.dispose();
  }

  void _reload() {
    setState(() {
      _itemsFuture = VideoDao.list();
    });
  }

  @override
  Widget build(BuildContext context) {
    return MediaQuery.removePadding(
      context: context,
      removeTop: true,
      child: 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 = VideoDao.list();
                setState(() => _itemsFuture = future);
                await future;
              },
              child: LayoutBuilder(
                builder: (context, constraints) {
                  final horizontalPadding = 30.w;
                  final crossAxisSpacing = 12.w;
                  final titleStyle = _VideoCard.createTitleStyle(context);
                  final titlePainter = TextPainter(
                    text: TextSpan(text: '视频标题', style: titleStyle),
                    maxLines: 1,
                    textDirection: Directionality.of(context),
                    textScaler: MediaQuery.textScalerOf(context),
                  )..layout();
                  final cardWidth = (constraints.maxWidth -
                          horizontalPadding * 2 -
                          crossAxisSpacing * 2) /
                      3;
                  final cardHeight =
                      cardWidth * 9 / 16 + 5.h + titlePainter.height;
                  return GridView.builder(
                    physics: const AlwaysScrollableScrollPhysics(),
                    padding: EdgeInsets.fromLTRB(
                      horizontalPadding,
                      10.h,
                      horizontalPadding,
                      16.h,
                    ),
                    gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                      crossAxisCount: 3,
                      mainAxisSpacing: 14.h,
                      crossAxisSpacing: crossAxisSpacing,
                      mainAxisExtent: cardHeight,
                    ),
                    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((_) {
      if (!mounted) return;
      unawaited(
        SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky),
      );
      _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: [
          AspectRatio(
            aspectRatio: 16 / 9,
            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,
          ),
        ],
      ),
    );
  }
}