From 40b88eeeb24561b619d02277bcfdd8f17767bf4c Mon Sep 17 00:00:00 2001 From: guozhigq Date: Wed, 19 Apr 2023 13:51:48 +0800 Subject: [PATCH] =?UTF-8?q?mod:=20hotPage=E5=BC=80=E5=8F=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/common/widgets/video_card_h.dart | 166 +++++++++++++++++++++++++++ lib/http/api.dart | 2 + lib/pages/hot/controller.dart | 59 ++++++++++ lib/pages/hot/view.dart | 62 +++++++++- lib/pages/main/view.dart | 19 ++- lib/utils/utils.dart | 92 +++++++++++++++ 6 files changed, 394 insertions(+), 6 deletions(-) create mode 100644 lib/common/widgets/video_card_h.dart diff --git a/lib/common/widgets/video_card_h.dart b/lib/common/widgets/video_card_h.dart new file mode 100644 index 00000000..b242b449 --- /dev/null +++ b/lib/common/widgets/video_card_h.dart @@ -0,0 +1,166 @@ +import 'package:get/get.dart'; +import 'package:flutter/material.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/stat/view.dart'; +import 'package:pilipala/utils/utils.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; + +// 视频卡片 - 水平布局 +class VideoCardH extends StatelessWidget { + var videoItem; + + VideoCardH({Key? key, required this.videoItem}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Material( + child: Ink( + child: InkWell( + onTap: () async { + await Future.delayed(const Duration(milliseconds: 200)); + int aid = videoItem['id'] ?? videoItem['aid']; + Get.toNamed('/video?aid=$aid', arguments: {'videoItem': videoItem}); + }, + child: Container( + padding: const EdgeInsets.fromLTRB( + StyleString.cardSpace, 5, StyleString.cardSpace, 5), + child: LayoutBuilder(builder: (context, boxConstraints) { + double width = + (boxConstraints.maxWidth - StyleString.cardSpace * 3) / 2; + return SizedBox( + height: width / StyleString.aspectRatio, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + // child: ClipRRect( + // borderRadius: StyleString.mdRadius, + child: LayoutBuilder( + builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + double PR = MediaQuery.of(context).devicePixelRatio; + return Stack( + children: [ + NetworkImgLayer( + // src: videoItem['pic'] + + // '@${(maxWidth * 2).toInt()}w', + src: videoItem['pic'] + '@.webp', + width: maxWidth, + height: maxHeight, + ), + // Image.network( videoItem['pic'], width: double.infinity, height: double.infinity,), + Positioned( + right: 4, + bottom: 4, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 1, horizontal: 6), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Colors.black54.withOpacity(0.4)), + child: Text( + Utils.timeFormat(videoItem['duration']!), + style: const TextStyle( + fontSize: 11, color: Colors.white), + ), + ), + ) + ], + ); + }, + ), + // ), + ), + VideoContent(videoItem: videoItem) + ], + ), + ); + }), + // height: 124, + ), + ), + ), + ); + } +} + +class VideoContent extends StatelessWidget { + final videoItem; + const VideoContent({super.key, required this.videoItem}); + + @override + Widget build(BuildContext context) { + return Expanded( + child: Padding( + padding: const EdgeInsets.fromLTRB(10, 2, 6, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + videoItem['title'], + textAlign: TextAlign.start, + style: TextStyle( + fontSize: Theme.of(context).textTheme.titleSmall!.fontSize, + fontWeight: FontWeight.w500), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const Spacer(), + if (videoItem['rcmd_reason'] != '' && + videoItem['rcmd_reason']['content'] != '') + Container( + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 5), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: Theme.of(context).colorScheme.surfaceTint), + ), + child: Text( + videoItem['rcmd_reason']['content'], + style: TextStyle( + fontSize: 9, + color: Theme.of(context).colorScheme.surfaceTint), + ), + ), + const SizedBox(height: 4), + Row( + children: [ + Image.asset( + 'assets/images/up_gray.png', + width: 14, + height: 12, + ), + const SizedBox(width: 2), + Text( + videoItem['owner']['name'], + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), + ), + ], + ), + Row( + children: [ + StatView( + theme: 'gray', + view: videoItem['stat']['view'], + ), + const SizedBox(width: 8), + Text( + Utils.dateFormat(videoItem['pubdate']!), + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.outline), + ) + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/http/api.dart b/lib/http/api.dart index 99abdbf4..ccea17f7 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -1,6 +1,8 @@ class Api { // 推荐视频 static const String recommendList = '/x/web-interface/index/top/feed/rcmd'; + // 热门视频 + static const String hotList = '/x/web-interface/popular'; // 视频详情 // 竖屏 https://api.bilibili.com/x/web-interface/view?aid=527403921 static const String videoDetail = '/x/web-interface/view'; diff --git a/lib/pages/hot/controller.dart b/lib/pages/hot/controller.dart index e69de29b..1df71824 100644 --- a/lib/pages/hot/controller.dart +++ b/lib/pages/hot/controller.dart @@ -0,0 +1,59 @@ +import 'package:flutter/animation.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/http/api.dart'; +import 'package:pilipala/http/init.dart'; + +class HotController extends GetxController { + final ScrollController scrollController = ScrollController(); + final int _count = 20; + int _currentPage = 1; + RxList videoList = [].obs; + bool isLoadingMore = false; + bool flag = false; + + @override + void onInit() { + super.onInit(); + queryHotFeed('init'); + } + + // 获取推荐 + Future queryHotFeed(type) async { + var res = await Request().get( + Api.hotList, + data: {'pn': _currentPage, 'ps': _count}, + ); + var data = res.data['data']['list']; + if (type == 'init') { + videoList.value = data; + } else if (type == 'onRefresh') { + videoList.insertAll(0, data); + } else if (type == 'onLoad') { + videoList.addAll(data); + } + _currentPage += 1; + isLoadingMore = false; + } + + // 下拉刷新 + Future onRefresh() async { + queryHotFeed('onRefresh'); + } + + // 上拉加载 + Future onLoad() async { + queryHotFeed('onLoad'); + } + + // 返回顶部并刷新 + void animateToTop() async { + if (scrollController.offset >= + MediaQuery.of(Get.context!).size.height * 5) { + scrollController.jumpTo(0); + } else { + await scrollController.animateTo(0, + duration: const Duration(milliseconds: 500), curve: Curves.easeInOut); + } + } +} diff --git a/lib/pages/hot/view.dart b/lib/pages/hot/view.dart index f820cccd..29a0b177 100644 --- a/lib/pages/hot/view.dart +++ b/lib/pages/hot/view.dart @@ -1,18 +1,70 @@ -import "package:flutter/material.dart"; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/video_card_h.dart'; +import 'package:pilipala/pages/hot/controller.dart'; +import 'package:pilipala/pages/home/widgets/app_bar.dart'; class HotPage extends StatefulWidget { - const HotPage({super.key}); + const HotPage({Key? key}) : super(key: key); @override State createState() => _HotPageState(); } -class _HotPageState extends State { +class _HotPageState extends State with AutomaticKeepAliveClientMixin { + final HotController _hotController = Get.put(HotController()); + List videoList = []; + + @override + bool get wantKeepAlive => true; + + @override + void initState() { + super.initState(); + _hotController.videoList.listen((value) { + videoList = value; + setState(() {}); + }); + + _hotController.scrollController.addListener( + () { + if (_hotController.scrollController.position.pixels >= + _hotController.scrollController.position.maxScrollExtent - 200) { + if (!_hotController.isLoadingMore) { + _hotController.isLoadingMore = true; + _hotController.onLoad(); + } + } + }, + ); + } + @override Widget build(BuildContext context) { + super.build(context); return Scaffold( - appBar: AppBar( - title: const Text('热门'), + body: RefreshIndicator( + displacement: kToolbarHeight + MediaQuery.of(context).padding.top, + onRefresh: () async { + return await _hotController.onRefresh(); + }, + child: CustomScrollView( + controller: _hotController.scrollController, + slivers: [ + const HomeAppBar(), + SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return VideoCardH( + videoItem: videoList[index], + ); + }, childCount: videoList.length)), + SliverToBoxAdapter( + child: SizedBox( + height: MediaQuery.of(context).padding.bottom + 10, + ), + ) + ], + ), ), ); } diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index 0ecb95f4..0b7556f4 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -// import 'package:pilipala/pages/home/controller.dart'; import 'package:pilipala/pages/home/index.dart'; +import 'package:pilipala/pages/hot/index.dart'; import './controller.dart'; class MainApp extends StatefulWidget { @@ -14,6 +14,7 @@ class MainApp extends StatefulWidget { class _MainAppState extends State with SingleTickerProviderStateMixin { final MainController _mainController = Get.put(MainController()); final HomeController _homeController = Get.put(HomeController()); + final HotController _hotController = Get.put(HotController()); late AnimationController? _animationController; late Animation? _fadeAnimation; @@ -63,6 +64,22 @@ class _MainAppState extends State with SingleTickerProviderStateMixin { } else { _homeController.flag = false; } + + if (currentPage is HotPage) { + if (_hotController.flag) { + // 单击返回顶部 双击并刷新 + if (DateTime.now().millisecondsSinceEpoch - _lastSelectTime! < 500) { + _hotController.onRefresh(); + } else { + await Future.delayed(const Duration(microseconds: 300)); + _hotController.animateToTop(); + } + _lastSelectTime = DateTime.now().millisecondsSinceEpoch; + } + _hotController.flag = true; + } else { + _hotController.flag = false; + } } @override diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 0d5918f5..2bd88366 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -38,4 +38,96 @@ class Utils { return ''; } } + + // 时间显示,刚刚,x分钟前 + static String dateFormat(timeStamp, {formatType = 'list'}) { + // 当前时间 + int time = (DateTime.now().millisecondsSinceEpoch / 1000).round(); + // 对比 + int _distance = (time - timeStamp).toInt(); + // 当前年日期 + String currentYearStr = 'MM月DD日 hh:mm'; + String lastYearStr = 'YY年MM月DD日 hh:mm'; + if (formatType == 'detail') { + currentYearStr = 'MM-DD hh:mm'; + lastYearStr = 'YY-MM-DD hh:mm'; + return CustomStamp_str( + timestamp: timeStamp, + date: lastYearStr, + toInt: false, + formatType: formatType); + } + if (_distance <= 60) { + return '刚刚'; + } else if (_distance <= 3600) { + return '${(_distance / 60).floor()}分钟前'; + } else if (_distance <= 43200) { + return '${(_distance / 60 / 60).floor()}小时前'; + } else if (DateTime.fromMillisecondsSinceEpoch(time * 1000).year == + DateTime.fromMillisecondsSinceEpoch(timeStamp * 1000).year) { + return CustomStamp_str( + timestamp: timeStamp, + date: currentYearStr, + toInt: false, + formatType: formatType); + } else { + return CustomStamp_str( + timestamp: timeStamp, + date: lastYearStr, + toInt: false, + formatType: formatType); + } + } + + // 时间戳转时间 + static String CustomStamp_str( + {int? timestamp, // 为空则显示当前时间 + String? date, // 显示格式,比如:'YY年MM月DD日 hh:mm:ss' + bool toInt = true, // 去除0开头 + String? formatType}) { + timestamp ??= (DateTime.now().millisecondsSinceEpoch / 1000).round(); + String timeStr = + (DateTime.fromMillisecondsSinceEpoch(timestamp * 1000)).toString(); + + dynamic dateArr = timeStr.split(' ')[0]; + dynamic timeArr = timeStr.split(' ')[1]; + + String YY = dateArr.split('-')[0]; + String MM = dateArr.split('-')[1]; + String DD = dateArr.split('-')[2]; + + String hh = timeArr.split(':')[0]; + String mm = timeArr.split(':')[1]; + String ss = timeArr.split(':')[2]; + + ss = ss.split('.')[0]; + + // 去除0开头 + if (toInt) { + MM = (int.parse(MM)).toString(); + DD = (int.parse(DD)).toString(); + hh = (int.parse(hh)).toString(); + mm = (int.parse(mm)).toString(); + } + + if (date == null) { + return timeStr; + } + + if (formatType == 'list' && int.parse(DD) > DateTime.now().day - 2) { + return '昨天'; + } + + date = date + .replaceAll('YY', YY) + .replaceAll('MM', MM) + .replaceAll('DD', DD) + .replaceAll('hh', hh) + .replaceAll('mm', mm) + .replaceAll('ss', ss); + if (int.parse(DD) < DateTime.now().day) { + return date.split(' ')[0]; + } + return date; + } }