From 5986add7dd335db72364450df140fc5610f4ca8f Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Tue, 4 Mar 2025 11:42:41 +0800 Subject: [PATCH] feat: show video note list Closes #376 Signed-off-by: bggRGjQaUbCoE --- lib/common/widgets/loading_widget.dart | 1 + lib/http/api.dart | 2 + lib/http/video.dart | 23 ++ lib/pages/member_search/search_dynamic.dart | 2 +- lib/pages/video/detail/controller.dart | 21 +- lib/pages/video/detail/introduction/view.dart | 11 +- .../video/detail/note/note_list_page.dart | 215 ++++++++++++++++++ .../video/detail/note/note_list_page_ctr.dart | 44 ++++ .../detail/reply/widgets/reply_item.dart | 8 +- lib/pages/video/detail/view.dart | 8 + lib/pages/video/detail/view_v.dart | 12 +- .../video/detail/widgets/header_control.dart | 10 + lib/utils/app_scheme.dart | 22 ++ 13 files changed, 363 insertions(+), 16 deletions(-) create mode 100644 lib/pages/video/detail/note/note_list_page.dart create mode 100644 lib/pages/video/detail/note/note_list_page_ctr.dart diff --git a/lib/common/widgets/loading_widget.dart b/lib/common/widgets/loading_widget.dart index fca7721a..f9f5122e 100644 --- a/lib/common/widgets/loading_widget.dart +++ b/lib/common/widgets/loading_widget.dart @@ -11,6 +11,7 @@ Widget errorWidget({errMsg, callback}) => HttpError( ); Widget scrollErrorWidget({errMsg, callback}) => CustomScrollView( + controller: ScrollController(), slivers: [ HttpError( errMsg: errMsg, diff --git a/lib/http/api.dart b/lib/http/api.dart index cddd08b0..d89b4741 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -719,4 +719,6 @@ class Api { static const String pgcIndexCondition = '/pgc/season/index/condition'; static const String pgcIndexResult = '/pgc/season/index/result'; + + static const String noteList = '/x/note/publish/list/archive'; } diff --git a/lib/http/video.dart b/lib/http/video.dart index 6e14d481..63567b85 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -1134,4 +1134,27 @@ class VideoHttp { return LoadingState.error(res.data['message']); } } + + static Future getVideoNoteList({ + dynamic oid, + dynamic uperMid, + required int page, + }) async { + var res = await Request().get( + Api.noteList, + queryParameters: { + 'csrf': await Request.getCsrf(), + 'oid': oid, + 'oid_type': 0, + 'pn': page, + 'ps': 10, + if (uperMid != null) 'uper_mid': uperMid, + }, + ); + if (res.data['code'] == 0) { + return LoadingState.success(res.data['data']); + } else { + return LoadingState.error(res.data['message']); + } + } } diff --git a/lib/pages/member_search/search_dynamic.dart b/lib/pages/member_search/search_dynamic.dart index ed6dac43..ad736ced 100644 --- a/lib/pages/member_search/search_dynamic.dart +++ b/lib/pages/member_search/search_dynamic.dart @@ -142,7 +142,7 @@ class SearchDynamic extends StatelessWidget { name, style: TextStyle( color: vip != null - ? (vip?['status'] ?? vip?['vipStatus']) > 0 && + ? (vip?['status'] ?? vip?['vipStatus'] ?? 0) > 0 && (vip?['type'] ?? vip?['vipType']) == 2 ? context.vipColor : null diff --git a/lib/pages/video/detail/controller.dart b/lib/pages/video/detail/controller.dart index e8f6e013..22f032c0 100644 --- a/lib/pages/video/detail/controller.dart +++ b/lib/pages/video/detail/controller.dart @@ -20,6 +20,7 @@ import 'package:PiliPlus/models/video/play/subtitle.dart'; import 'package:PiliPlus/models/video_detail_res.dart'; import 'package:PiliPlus/pages/search/widgets/search_text.dart'; import 'package:PiliPlus/pages/video/detail/introduction/controller.dart'; +import 'package:PiliPlus/pages/video/detail/note/note_list_page.dart'; import 'package:PiliPlus/pages/video/detail/related/controller.dart'; import 'package:PiliPlus/pages/video/detail/reply/controller.dart'; import 'package:PiliPlus/pages/video/detail/view_v.dart' show ViewPointsPage; @@ -1343,9 +1344,9 @@ class VideoDetailController extends GetxController childKey.currentState?.showBottomSheet( enableDrag: false, backgroundColor: Colors.transparent, - (context) => ViewPointsPage( - child: _postPanel(), - ), + (context) => GStorage.collapsibleVideoPage + ? ViewPointsPage(child: _postPanel()) + : _postPanel(), ); } } @@ -2175,4 +2176,18 @@ class VideoDetailController extends GetxController debugPrint('_getDmTrend: $e'); } } + + void showNoteList() async { + if (plPlayerController.isFullScreen.value) { + Utils.showFSSheet( + child: NoteListPage(oid: oid.value), + isFullScreen: plPlayerController.isFullScreen.value, + ); + } else { + childKey.currentState?.showBottomSheet( + backgroundColor: Colors.transparent, + (context) => NoteListPage(oid: oid.value), + ); + } + } } diff --git a/lib/pages/video/detail/introduction/view.dart b/lib/pages/video/detail/introduction/view.dart index 959936ce..bd72267d 100644 --- a/lib/pages/video/detail/introduction/view.dart +++ b/lib/pages/video/detail/introduction/view.dart @@ -406,12 +406,11 @@ class _VideoInfoState extends State with TickerProviderStateMixin { 'status'] ?? -1) > 0 && - (videoIntroController - .userStat - .value['card']?['vip'] - ?[ - 'type'] ?? - -1) == + videoIntroController + .userStat + .value['card'] + ?[ + 'vip']?['type'] == 2 ? context.vipColor : null, diff --git a/lib/pages/video/detail/note/note_list_page.dart b/lib/pages/video/detail/note/note_list_page.dart new file mode 100644 index 00000000..3eb018b1 --- /dev/null +++ b/lib/pages/video/detail/note/note_list_page.dart @@ -0,0 +1,215 @@ +import 'package:PiliPlus/common/skeleton/video_reply.dart'; +import 'package:PiliPlus/common/widgets/icon_button.dart'; +import 'package:PiliPlus/common/widgets/loading_widget.dart'; +import 'package:PiliPlus/common/widgets/network_img_layer.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/pages/video/detail/note/note_list_page_ctr.dart'; +import 'package:PiliPlus/utils/app_scheme.dart'; +import 'package:PiliPlus/utils/extension.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class NoteListPage extends StatefulWidget { + const NoteListPage({super.key, this.oid, this.upperMid}); + + final dynamic oid; + final dynamic upperMid; + + @override + State createState() => _NoteListPageState(); +} + +class _NoteListPageState extends State { + late final _controller = Get.put( + NoteListPageCtr(oid: widget.oid, upperMid: widget.upperMid), + ); + + @override + void dispose() { + Get.delete(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + automaticallyImplyLeading: false, + titleSpacing: 16, + toolbarHeight: 45, + title: Obx( + () => Text( + '笔记${_controller.count.value == -1 ? '' : '(${_controller.count.value})'}'), + ), + bottom: PreferredSize( + preferredSize: Size.fromHeight(1), + child: Divider( + height: 1, + color: Theme.of(context).colorScheme.outline.withOpacity(0.1), + ), + ), + actions: [ + iconButton( + context: context, + tooltip: '关闭', + icon: Icons.clear, + onPressed: Get.back, + size: 32, + ), + const SizedBox(width: 16), + ], + ), + body: Obx(() => _buildBody(_controller.loadingState.value)), + ); + } + + Widget _buildBody(LoadingState loadingState) { + return switch (loadingState) { + Loading() => CustomScrollView( + physics: const NeverScrollableScrollPhysics(), + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return const VideoReplySkeleton(); + }, + childCount: 8, + ), + ) + ], + ), + Success() => (loadingState.response as List?)?.isNotEmpty == true + ? RefreshIndicator( + onRefresh: () async { + await _controller.onRefresh(); + }, + child: CustomScrollView( + controller: ScrollController(), + slivers: [ + SliverList.separated( + itemBuilder: (context, index) { + if (index == loadingState.response.length - 1) { + _controller.onLoadMore(); + } + return _itemWidget(context, loadingState.response[index]); + }, + itemCount: loadingState.response.length, + separatorBuilder: (context, index) => Divider( + height: 1, + color: Theme.of(context) + .colorScheme + .outline + .withOpacity(0.1), + ), + ), + SliverToBoxAdapter( + child: SizedBox( + height: MediaQuery.paddingOf(context).bottom + 80, + ), + ), + ], + ), + ) + : scrollErrorWidget( + callback: _controller.onReload, + ), + Error() => scrollErrorWidget( + errMsg: loadingState.errMsg, + callback: _controller.onReload, + ), + LoadingState() => throw UnimplementedError(), + }; + } +} + +Widget _itemWidget(BuildContext context, dynamic item) { + return InkWell( + onTap: () { + if (item['web_url'] != null && item['web_url'] != '') { + PiliScheme.routePushFromUrl(item['web_url']); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + Get.toNamed('/member?mid=${item['author']['mid']}'); + }, + child: NetworkImgLayer( + height: 34, + width: 34, + src: item['author']['face'], + type: 'avatar', + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () { + Get.toNamed('/member?mid=${item['author']['mid']}'); + }, + child: Row( + children: [ + Text( + item['author']['name'], + style: TextStyle( + color: (item['author']?['vip_info']?['status'] ?? 0) > + 0 && + item['author']?['vip_info']?['type'] == 2 + ? context.vipColor + : Theme.of(context).colorScheme.outline, + fontSize: 13, + ), + ), + const SizedBox(width: 6), + Image.asset( + 'assets/images/lv/lv${item['author']['level']}.png', + height: 11, + ), + ], + ), + ), + const SizedBox(height: 4), + Text( + item['pubtime'], + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + fontSize: 12, + ), + ), + if (item['summary'] != null) ...[ + const SizedBox(height: 5), + Text( + item['summary'], + style: TextStyle( + height: 1.75, + fontSize: + Theme.of(context).textTheme.bodyMedium!.fontSize, + ), + ), + if (item['web_url'] != null && item['web_url'] != '') + Text( + '查看全部', + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + height: 1.75, + fontSize: + Theme.of(context).textTheme.bodyMedium!.fontSize, + ), + ), + ], + ], + ), + ), + ], + ), + ), + ); +} diff --git a/lib/pages/video/detail/note/note_list_page_ctr.dart b/lib/pages/video/detail/note/note_list_page_ctr.dart new file mode 100644 index 00000000..3cfaaacb --- /dev/null +++ b/lib/pages/video/detail/note/note_list_page_ctr.dart @@ -0,0 +1,44 @@ +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/video.dart'; +import 'package:PiliPlus/pages/common/common_controller.dart'; +import 'package:PiliPlus/utils/extension.dart'; +import 'package:get/get.dart'; + +class NoteListPageCtr extends CommonController { + NoteListPageCtr({this.oid, this.upperMid}); + final dynamic oid; + final dynamic upperMid; + + RxInt count = (-1).obs; + + @override + void onInit() { + super.onInit(); + queryData(); + } + + @override + bool customHandleResponse(Success response) { + dynamic data = response.response; + count.value = data['page']?['total'] ?? -1; + if ((data['list'] as List?).isNullOrEmpty) { + isEnd = true; + } + if (currentPage != 1 && loadingState.value is Success) { + data['list'] ??= []; + data['list'].insertAll(0, (loadingState.value as Success).response); + } + if (isEnd.not && count.value != -1 && data['list'].length >= count.value) { + isEnd = true; + } + loadingState.value = LoadingState.success(data['list']); + return true; + } + + @override + Future customGetData() => VideoHttp.getVideoNoteList( + oid: oid, + uperMid: upperMid, + page: currentPage, + ); +} diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index 3f75de17..c4de591b 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -266,9 +266,11 @@ class ReplyItem extends StatelessWidget { Text( '${replyItem.member?.uname}', style: TextStyle( - color: (replyItem.member?.vip?['vipType'] == 2) - ? context.vipColor - : Theme.of(context).colorScheme.outline, + color: + (replyItem.member?.vip?['vipStatus'] ?? 0) > 0 && + replyItem.member?.vip?['vipType'] == 2 + ? context.vipColor + : Theme.of(context).colorScheme.outline, fontSize: 13, ), ), diff --git a/lib/pages/video/detail/view.dart b/lib/pages/video/detail/view.dart index e0fe3544..b4fffed2 100644 --- a/lib/pages/video/detail/view.dart +++ b/lib/pages/video/detail/view.dart @@ -1104,6 +1104,9 @@ class _VideoDetailPageState extends State }); } break; + case 'note': + videoDetailController.showNoteList(); + break; } }, itemBuilder: (BuildContext context) => @@ -1112,6 +1115,11 @@ class _VideoDetailPageState extends State value: 'later', child: Text('稍后再看'), ), + if (videoDetailController.epId == null) + const PopupMenuItem( + value: 'note', + child: Text('查看笔记'), + ), const PopupMenuItem( value: 'report', child: Text('举报'), diff --git a/lib/pages/video/detail/view_v.dart b/lib/pages/video/detail/view_v.dart index e368f13a..49d46b6e 100644 --- a/lib/pages/video/detail/view_v.dart +++ b/lib/pages/video/detail/view_v.dart @@ -1397,6 +1397,9 @@ class _VideoDetailPageVState extends State }); } break; + case 'note': + videoDetailController.showNoteList(); + break; } }, itemBuilder: (BuildContext context) => @@ -1405,6 +1408,11 @@ class _VideoDetailPageVState extends State value: 'later', child: Text('稍后再看'), ), + if (videoDetailController.epId == null) + const PopupMenuItem( + value: 'note', + child: Text('查看笔记'), + ), const PopupMenuItem( value: 'report', child: Text('举报'), @@ -2404,9 +2412,7 @@ class _VideoDetailPageVState extends State videoDetailController.childKey.currentState?.showBottomSheet( backgroundColor: Colors.transparent, (context) => GStorage.collapsibleVideoPage - ? ViewPointsPage( - child: listSheetContent(), - ) + ? ViewPointsPage(child: listSheetContent()) : listSheetContent(), ); } diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index 5096efdc..000b6b8a 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -157,6 +157,16 @@ class _HeaderControlState extends State { leading: const Icon(Icons.watch_later_outlined, size: 20), title: const Text('添加至「稍后再看」', style: titleStyle), ), + if (widget.videoDetailCtr.epId == null) + ListTile( + dense: true, + onTap: () { + Get.back(); + widget.videoDetailCtr.showNoteList(); + }, + leading: const Icon(Icons.note_alt_outlined, size: 20), + title: const Text('查看笔记', style: titleStyle), + ), ListTile( dense: true, onTap: () => {Get.back(), scheduleExit()}, diff --git a/lib/utils/app_scheme.dart b/lib/utils/app_scheme.dart index e20791c6..382cf274 100644 --- a/lib/utils/app_scheme.dart +++ b/lib/utils/app_scheme.dart @@ -430,7 +430,29 @@ class PiliScheme { final String? area = pathSegments.first == 'mobile' ? pathSegments.getOrNull(1) : pathSegments.first; + debugPrint('area: $area'); switch (area) { + case 'h5': + if (path.startsWith('/h5/note')) { + String? id = RegExp(r'cvid=(\d+)', caseSensitive: false) + .firstMatch(uri.query) + ?.group(1); + if (id != null) { + Utils.toDupNamed( + '/htmlRender', + parameters: { + 'url': 'https://www.bilibili.com/read/cv$id', + 'title': '', + 'id': 'cv$id', + 'dynamicType': 'read' + }, + off: off, + ); + return true; + } + } + launchURL(); + return false; case 'opus' || 'dynamic': bool hasMatch = await _onPushDynDetail(path, off); if (hasMatch.not) {