From d1aa0144c9289fe4359cfd132afef62228623f40 Mon Sep 17 00:00:00 2001 From: guozhigq Date: Mon, 5 Jun 2023 17:22:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A7=82=E7=9C=8B=E8=AE=B0=E5=BD=95?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/http/user.dart | 18 ++ lib/models/user/history.dart | 168 ++++++++++++++++++ lib/pages/history/controller.dart | 44 +++++ lib/pages/history/index.dart | 4 + lib/pages/history/view.dart | 94 ++++++++++ lib/pages/history/widgets/item.dart | 154 ++++++++++++++++ lib/pages/media/controller.dart | 2 +- lib/pages/video/detail/controller.dart | 10 +- lib/pages/video/detail/introduction/view.dart | 17 +- lib/pages/video/detail/reply/controller.dart | 2 +- lib/router/app_pages.dart | 3 + 11 files changed, 506 insertions(+), 10 deletions(-) create mode 100644 lib/models/user/history.dart create mode 100644 lib/pages/history/controller.dart create mode 100644 lib/pages/history/index.dart create mode 100644 lib/pages/history/view.dart create mode 100644 lib/pages/history/widgets/item.dart diff --git a/lib/http/user.dart b/lib/http/user.dart index b6a52af8..6d4e8100 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -1,8 +1,11 @@ +import 'dart:developer'; + import 'package:pilipala/http/api.dart'; import 'package:pilipala/http/init.dart'; import 'package:pilipala/models/model_hot_video_item.dart'; import 'package:pilipala/models/user/fav_detail.dart'; import 'package:pilipala/models/user/fav_folder.dart'; +import 'package:pilipala/models/user/history.dart'; import 'package:pilipala/models/user/info.dart'; import 'package:pilipala/models/user/stat.dart'; @@ -94,4 +97,19 @@ class UserHttp { return {'status': false, 'data': [], 'msg': res.data['message']}; } } + + // 观看历史 + static Future historyList(int? max, int? viewAt) async { + var res = await Request().get(Api.historyList, data: { + 'type': 'all', + 'ps': 20, + 'max': max ?? 0, + 'view_at': viewAt ?? 0, + }); + if (res.data['code'] == 0) { + return {'status': true, 'data': HistoryData.fromJson(res.data['data'])}; + } else { + return {'status': false, 'data': [], 'msg': res.data['message']}; + } + } } diff --git a/lib/models/user/history.dart b/lib/models/user/history.dart new file mode 100644 index 00000000..7127da49 --- /dev/null +++ b/lib/models/user/history.dart @@ -0,0 +1,168 @@ +class HistoryData { + HistoryData({ + this.cursor, + this.tab, + this.list, + }); + + Cursor? cursor; + List? tab; + List? list; + + HistoryData.fromJson(Map json) { + cursor = Cursor.fromJson(json['cursor']); + tab = json['tab'].map((e) => HisTabItem.fromJson(e)).toList(); + list = + json['list'].map((e) => HisListItem.fromJson(e)).toList(); + } +} + +class Cursor { + Cursor({ + this.max, + this.viewAt, + this.business, + this.ps, + }); + + int? max; + int? viewAt; + String? business; + int? ps; + + Cursor.fromJson(Map json) { + max = json['max']; + viewAt = json['view_at']; + business = json['business']; + ps = json['ps']; + } +} + +class HisTabItem { + HisTabItem({ + this.type, + this.name, + }); + + String? type; + String? name; + + HisTabItem.fromJson(Map json) { + type = json['type']; + name = json['name']; + } +} + +class HisListItem { + HisListItem({ + this.title, + this.longTitle, + this.cover, + this.pic, + this.covers, + this.uri, + this.history, + this.videos, + this.authorName, + this.authorFace, + this.authorMid, + this.viewAt, + this.progress, + this.badge, + this.showTitle, + this.duration, + this.current, + this.total, + this.newDesc, + this.isFinish, + this.isFav, + this.kid, + this.tagName, + this.liveStatus, + }); + + String? title; + String? longTitle; + String? cover; + String? pic; + String? covers; + String? uri; + History? history; + int? videos; + String? authorName; + String? authorFace; + int? authorMid; + int? viewAt; + int? progress; + String? badge; + String? showTitle; + int? duration; + String? current; + int? total; + String? newDesc; + int? isFinish; + int? isFav; + int? kid; + String? tagName; + int? liveStatus; + + HisListItem.fromJson(Map json) { + title = json['title']; + longTitle = json['long_title']; + cover = json['cover']; + pic = json['cover'] ?? ''; + covers = json['covers'] ?? ''; + uri = json['uri']; + history = History.fromJson(json['history']); + videos = json['videos']; + authorName = json['author_name']; + authorFace = json['author_face']; + authorMid = json['author_mid']; + viewAt = json['view_at']; + progress = json['progress']; + badge = json['badge']; + showTitle = json['show_title']; + duration = json['duration']; + current = json['current']; + total = json['total']; + newDesc = json['new_desc']; + isFinish = json['is_finish']; + isFav = json['is_fav']; + kid = json['kid']; + tagName = json['tag_name']; + liveStatus = json['live_status']; + } +} + +class History { + History({ + this.oid, + this.epid, + this.bvid, + this.page, + this.cid, + this.part, + this.business, + this.dt, + }); + + int? oid; + int? epid; + String? bvid; + int? page; + int? cid; + String? part; + String? business; + int? dt; + + History.fromJson(Map json) { + oid = json['oid']; + epid = json['epid']; + bvid = json['bvid']; + page = json['page']; + cid = json['cid']; + part = json['part']; + business = json['business']; + dt = json['dt']; + } +} diff --git a/lib/pages/history/controller.dart b/lib/pages/history/controller.dart new file mode 100644 index 00000000..7f26ba40 --- /dev/null +++ b/lib/pages/history/controller.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/http/user.dart'; +import 'package:pilipala/models/user/history.dart'; + +class HistoryController extends GetxController { + final ScrollController scrollController = ScrollController(); + RxList historyList = [HisListItem()].obs; + bool isLoadingMore = false; + + @override + void onInit() { + queryHistoryList(); + super.onInit(); + } + + Future queryHistoryList({type = 'init'}) async { + int max = 0; + int viewAt = 0; + if (type == 'onload') { + max = historyList.last.history!.oid!; + viewAt = historyList.last.viewAt!; + } + isLoadingMore = true; + var res = await UserHttp.historyList(max, viewAt); + isLoadingMore = false; + if (res['status']) { + if (type == 'onload') { + historyList.addAll(res['data'].list); + } else { + historyList.value = res['data'].list; + } + } + return res; + } + + Future onLoad() async { + queryHistoryList(type: 'onload'); + } + + Future onRefresh() async { + queryHistoryList(type: 'onRefresh'); + } +} diff --git a/lib/pages/history/index.dart b/lib/pages/history/index.dart new file mode 100644 index 00000000..ff8fb640 --- /dev/null +++ b/lib/pages/history/index.dart @@ -0,0 +1,4 @@ +library history; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/history/view.dart b/lib/pages/history/view.dart new file mode 100644 index 00000000..f0142a3e --- /dev/null +++ b/lib/pages/history/view.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/skeleton/video_card_h.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/pages/history/index.dart'; + +import 'widgets/item.dart'; + +class HistoryPage extends StatefulWidget { + const HistoryPage({super.key}); + + @override + State createState() => _HistoryPageState(); +} + +class _HistoryPageState extends State { + final HistoryController _historyController = Get.put(HistoryController()); + Future? _futureBuilderFuture; + + @override + void initState() { + _futureBuilderFuture = _historyController.queryHistoryList(); + super.initState(); + + _historyController.scrollController.addListener( + () { + if (_historyController.scrollController.position.pixels >= + _historyController.scrollController.position.maxScrollExtent - + 300) { + if (!_historyController.isLoadingMore) { + _historyController.onLoad(); + } + } + }, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('观看记录'), + centerTitle: false, + ), + body: RefreshIndicator( + onRefresh: () async { + await _historyController.onRefresh(); + return; + }, + child: CustomScrollView( + controller: _historyController.scrollController, + slivers: [ + FutureBuilder( + future: _futureBuilderFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + Map data = snapshot.data; + if (data['status']) { + return Obx( + () => SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return HistoryItem( + videoItem: _historyController.historyList[index], + ); + }, childCount: _historyController.historyList.length), + ), + ); + } else { + return HttpError( + errMsg: data['msg'], + fn: () => setState(() {}), + ); + } + } else { + // 骨架屏 + return SliverList( + delegate: SliverChildBuilderDelegate((context, index) { + return const VideoCardHSkeleton(); + }, childCount: 10), + ); + } + }, + ), + SliverToBoxAdapter( + child: SizedBox( + height: MediaQuery.of(context).padding.bottom + 10, + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/pages/history/widgets/item.dart b/lib/pages/history/widgets/item.dart new file mode 100644 index 00000000..deddabdf --- /dev/null +++ b/lib/pages/history/widgets/item.dart @@ -0,0 +1,154 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/constants.dart'; +import 'package:pilipala/common/widgets/network_img_layer.dart'; +import 'package:pilipala/utils/utils.dart'; + +class HistoryItem extends StatelessWidget { + var videoItem; + HistoryItem({super.key, required this.videoItem}); + + @override + Widget build(BuildContext context) { + int aid = videoItem.history.oid; + String heroTag = Utils.makeHeroTag(aid); + return InkWell( + onTap: () async { + await Future.delayed(const Duration(milliseconds: 200)); + Get.toNamed('/video?aid=$aid', + arguments: {'heroTag': heroTag, 'pic': videoItem.cover}); + }, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.fromLTRB( + StyleString.cardSpace, 7, StyleString.cardSpace, 7), + child: LayoutBuilder( + builder: (context, boxConstraints) { + double width = + (boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2; + return SizedBox( + height: width / StyleString.aspectRatio, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: (context, boxConstraints) { + double maxWidth = boxConstraints.maxWidth; + double maxHeight = boxConstraints.maxHeight; + double PR = MediaQuery.of(context).devicePixelRatio; + return Stack( + children: [ + Hero( + tag: heroTag, + child: NetworkImgLayer( + // src: videoItem['pic'] + + // '@${(maxWidth * 2).toInt()}w', + src: videoItem.cover + '@.webp', + width: maxWidth, + height: maxHeight, + ), + ), + 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) + ], + ), + ); + }, + ), + ), + Divider( + height: 1, + indent: 8, + endIndent: 12, + color: Theme.of(context).dividerColor.withOpacity(0.08), + ) + ], + ), + ); + } +} + +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: videoItem.videos > 1 ? 1 : 2, + overflow: TextOverflow.ellipsis, + ), + if (videoItem.videos > 1) + Text( + videoItem.showTitle, + textAlign: TextAlign.start, + style: TextStyle( + fontSize: Theme.of(context).textTheme.titleSmall!.fontSize, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.outline), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const Spacer(), + Row( + children: [ + Text( + videoItem.authorName, + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, + color: Theme.of(context).colorScheme.outline, + ), + ), + ], + ), + Row( + children: [ + Text( + Utils.dateFormat(videoItem.viewAt!), + style: TextStyle( + fontSize: 11, + color: Theme.of(context).colorScheme.outline), + ) + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/media/controller.dart b/lib/pages/media/controller.dart index bb2e3661..6b0d5b77 100644 --- a/lib/pages/media/controller.dart +++ b/lib/pages/media/controller.dart @@ -18,7 +18,7 @@ class MediaController extends GetxController { { 'icon': Icons.history, 'title': '观看记录', - 'onTap': () {}, + 'onTap': () => Get.toNamed('/history'), }, { 'icon': Icons.star_border, diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index 6c69a993..faeda285 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -41,18 +41,22 @@ class VideoDetailController extends GetxController { videoItem['pic'] = args.pic; } } + if (Get.arguments.containsKey('pic')) { + videoItem['pic'] = Get.arguments['pic']; + } heroTag = Get.arguments['heroTag']; } } showReplyReplyPanel() { - PersistentBottomSheetController? ctr = scaffoldKey.currentState?.showBottomSheet((BuildContext context) { + PersistentBottomSheetController? ctr = + scaffoldKey.currentState?.showBottomSheet((BuildContext context) { return VideoReplyReplyPanel( oid: oid, rpid: fRpid, - closePanel: ()=> { + closePanel: () => { fRpid = 0, - }, + }, firstFloor: firstFloor, ); }); diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 2fbda43c..bba8d137 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -380,11 +380,18 @@ class _VideoInfoState extends State with TickerProviderStateMixin { duration: const Duration(milliseconds: 150), child: SizedBox( height: 36, - child: Obx(()=> - videoIntroController.followStatus.isNotEmpty ? ElevatedButton( - onPressed: () => videoIntroController.actionRelationMod(), - child: Text(videoIntroController.followStatus['attribute'] == 0 ? '关注' : '已关注'), - ) : const SizedBox(), + child: Obx( + () => videoIntroController.followStatus.isNotEmpty + ? ElevatedButton( + onPressed: () => videoIntroController + .actionRelationMod(), + child: Text(videoIntroController + .followStatus['attribute'] == + 0 + ? '关注' + : '已关注'), + ) + : const SizedBox(), ), ), ), diff --git a/lib/pages/video/detail/reply/controller.dart b/lib/pages/video/detail/reply/controller.dart index c2c5d3aa..63a15958 100644 --- a/lib/pages/video/detail/reply/controller.dart +++ b/lib/pages/video/detail/reply/controller.dart @@ -45,7 +45,7 @@ class VideoReplyController extends GetxController { if (res['data'].replies.isNotEmpty) { currentPage = currentPage + 1; noMore.value = '加载中'; - if(res['data'].page.count == res['data'].page.acount){ + if (res['data'].page.count == res['data'].page.acount) { noMore.value = '没有更多了'; } } else { diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 0edbc14f..7e6c1a45 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import 'package:pilipala/pages/fav/index.dart'; import 'package:pilipala/pages/favDetail/index.dart'; +import 'package:pilipala/pages/history/index.dart'; import 'package:pilipala/pages/home/index.dart'; import 'package:pilipala/pages/hot/index.dart'; import 'package:pilipala/pages/later/index.dart'; @@ -38,5 +39,7 @@ class Routes { GetPage(name: '/favDetail', page: () => const FavDetailPage()), // 稍后再看 GetPage(name: '/later', page: () => const LaterPage()), + // 历史记录 + GetPage(name: '/history', page: () => const HistoryPage()), ]; }