diff --git a/lib/http/video.dart b/lib/http/video.dart index 98d42b1fa..2aa748842 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -1,6 +1,8 @@ import 'dart:convert'; import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' + show ReplyInfo; import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/browser_ua.dart'; import 'package:PiliPlus/http/init.dart'; @@ -32,7 +34,10 @@ import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/recommend_filter.dart'; +import 'package:PiliPlus/utils/request_utils.dart'; +import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/wbi_sign.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart' show compute; @@ -524,7 +529,7 @@ abstract final class VideoHttp { // parent num 父评论rpid 非必要 二级评论同根评论id 大于二级评论为要回复的评论id // message str 发送评论内容 必要 最大1000字符 // plat num 发送平台标识 非必要 1:web端 2:安卓客户端 3:ios客户端 4:wp客户端 - static Future> replyAdd({ + static Future> replyAdd({ required int type, required int oid, required String message, @@ -552,7 +557,19 @@ abstract final class VideoHttp { options: Options(contentType: Headers.formUrlEncodedContentType), ); if (res.data['code'] == 0) { - return Success(res.data['data']); + try { + final replyInfo = RequestUtils.replyCast(res.data['data']['reply']); + GStorage.reply.put( + replyInfo.id.toString(), + (replyInfo.toProto3Json() as Map) + ..remove('memberV2') + ..remove('trackInfo'), + ); + return Success(replyInfo); + } catch (e, s) { + Utils.reportError(e, s); + return const Success(null); + } } else { return Error(res.data['message']); } @@ -574,6 +591,7 @@ abstract final class VideoHttp { options: Options(contentType: Headers.formUrlEncodedContentType), ); if (res.data['code'] == 0) { + GStorage.reply.delete(rpid.toString()); return const Success(null); } else { return const Error('请退出账号后重新登录'); diff --git a/lib/pages/common/reply_controller.dart b/lib/pages/common/reply_controller.dart index 875db456f..7b0ad4ba5 100644 --- a/lib/pages/common/reply_controller.dart +++ b/lib/pages/common/reply_controller.dart @@ -10,7 +10,6 @@ import 'package:PiliPlus/pages/common/publish/publish_route.dart'; import 'package:PiliPlus/pages/video/reply_new/view.dart'; import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/reply_utils.dart'; -import 'package:PiliPlus/utils/request_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; @@ -174,10 +173,9 @@ abstract class ReplyController extends CommonListController { ), ) .then( - (res) { - if (res != null) { + (replyInfo) { + if (replyInfo is ReplyInfo) { savedReplies.remove(key); - ReplyInfo replyInfo = RequestUtils.replyCast(res); if (loadingState.value case Success(:final response)) { if (response == null) { loadingState.value = Success([replyInfo]); diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index c07f8ae72..b95c2d151 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -160,6 +160,14 @@ class _MediaPageState extends CommonPageState ), msgBadge(_mainController), ], + IconButton( + iconSize: iconSize, + padding: padding, + style: style, + tooltip: '评论记录', + onPressed: () => Get.toNamed('/myReply'), + icon: const Icon(Icons.message_outlined), + ), Obx( () { final anonymity = MineController.anonymity.value; diff --git a/lib/pages/my_reply/view.dart b/lib/pages/my_reply/view.dart new file mode 100644 index 000000000..d249ea9a8 --- /dev/null +++ b/lib/pages/my_reply/view.dart @@ -0,0 +1,122 @@ +import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; +import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart'; +import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart'; +import 'package:PiliPlus/utils/app_scheme.dart'; +import 'package:PiliPlus/utils/page_utils.dart'; +import 'package:PiliPlus/utils/reply_utils.dart'; +import 'package:PiliPlus/utils/storage.dart'; +import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:PiliPlus/utils/waterfall.dart'; +import 'package:flutter/foundation.dart' show kDebugMode; +import 'package:flutter/material.dart'; +import 'package:waterfall_flow/waterfall_flow.dart'; + +class MyReply extends StatefulWidget { + const MyReply({super.key}); + + @override + State createState() => _MyReplyState(); +} + +class _MyReplyState extends State with DynMixin { + late final List _replies; + + @override + void initState() { + super.initState(); + _replies = + GStorage.reply.values + .map((e) => ReplyInfo.create()..mergeFromProto3Json(e)) + .toList() + ..sort((a, b) => b.ctime.compareTo(a.ctime)); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('我的评论'), + actions: kDebugMode + ? [ + IconButton( + tooltip: 'Clear', + onPressed: () => showConfirmDialog( + context: context, + title: 'Clear Local Storage?', + onConfirm: () { + GStorage.reply.clear(); + _replies.clear(); + setState(() {}); + }, + ), + icon: const Icon(Icons.clear_all), + ), + const SizedBox(width: 6), + ] + : null, + ), + body: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + _replies.isNotEmpty + ? ViewSliverSafeArea( + sliver: SliverWaterfallFlow( + gridDelegate: dynGridDelegate, + delegate: SliverChildBuilderDelegate( + childCount: _replies.length, + (context, index) => ReplyItemGrpc( + replyLevel: 0, + needDivider: false, + replyItem: _replies[index], + replyReply: _replyReply, + onDelete: (_, _) => _onDelete(index), + onCheckReply: _onCheckReply, + ), + ), + ), + ) + : const HttpError(), + ], + ), + ); + } + + void _replyReply(ReplyInfo replyInfo, int? rpid) { + switch (replyInfo.type.toInt()) { + case 1: + PiliScheme.videoPush( + replyInfo.oid.toInt(), + null, + ); + case 12: + PageUtils.toDupNamed( + '/articlePage', + parameters: { + 'id': replyInfo.oid.toString(), + 'type': 'read', + }, + ); + case _: + PageUtils.pushDynFromId( + rid: replyInfo.oid.toString(), + type: replyInfo.type, + ); + } + } + + void _onDelete(int index) { + _replies.removeAt(index); + setState(() {}); + } + + void _onCheckReply(ReplyInfo replyInfo) { + ReplyUtils.onCheckReply( + replyInfo: replyInfo, + biliSendCommAntifraud: Pref.biliSendCommAntifraud, + sourceId: null, + isManual: true, + ); + } +} diff --git a/lib/pages/video/reply/widgets/reply_item_grpc.dart b/lib/pages/video/reply/widgets/reply_item_grpc.dart index 65cba75d6..3fd43bd8d 100644 --- a/lib/pages/video/reply/widgets/reply_item_grpc.dart +++ b/lib/pages/video/reply/widgets/reply_item_grpc.dart @@ -29,6 +29,7 @@ import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/url_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; @@ -865,6 +866,35 @@ class ReplyItemGrpc extends StatelessWidget { ), ), ), + if (kDebugMode) ...[ + ListTile( + onTap: () { + Get.back(); + GStorage.reply.put( + item.id.toString(), + (item.toProto3Json() as Map) + ..remove('replies') + ..remove('memberV2') + ..remove('trackInfo'), + ); + }, + title: Text( + 'save to local', + style: style!.copyWith(color: theme.colorScheme.primary), + ), + ), + ListTile( + onTap: () { + Get.back(); + onDelete(); + GStorage.reply.delete(item.id.toString()); + }, + title: Text( + 'remove from local', + style: style.copyWith(color: theme.colorScheme.primary), + ), + ), + ], if (ownerMid == upMid || ownerMid == item.member.mid) ListTile( onTap: () async { diff --git a/lib/pages/video/reply_new/view.dart b/lib/pages/video/reply_new/view.dart index b7fb4fae6..30efcdbcb 100644 --- a/lib/pages/video/reply_new/view.dart +++ b/lib/pages/video/reply_new/view.dart @@ -429,8 +429,8 @@ class _ReplyPageState extends CommonRichTextPubPageState { ); if (res case Success(:final response)) { hasPub = true; - SmartDialog.showToast(response['success_toast']); - Get.back(result: response['reply']); + SmartDialog.showToast('发送成功'); + Get.back(result: response); } else { res.toast(); } diff --git a/lib/pages/video/reply_reply/controller.dart b/lib/pages/video/reply_reply/controller.dart index 5f2e08312..29ebcded3 100644 --- a/lib/pages/video/reply_reply/controller.dart +++ b/lib/pages/video/reply_reply/controller.dart @@ -6,7 +6,6 @@ import 'package:PiliPlus/pages/common/publish/publish_route.dart'; import 'package:PiliPlus/pages/common/reply_controller.dart'; import 'package:PiliPlus/pages/video/reply_new/view.dart'; import 'package:PiliPlus/utils/id_utils.dart'; -import 'package:PiliPlus/utils/request_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:fixnum/fixnum.dart'; @@ -195,10 +194,9 @@ class VideoReplyReplyController extends ReplyController }, ), ) - .then((res) { - if (res != null) { + .then((replyInfo) { + if (replyInfo is ReplyInfo) { savedReplies.remove(key); - ReplyInfo replyInfo = RequestUtils.replyCast(res); count.value += 1; loadingState diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 22292df3d..25164d77c 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -43,6 +43,7 @@ import 'package:PiliPlus/pages/msg_feed_top/like_me/view.dart'; import 'package:PiliPlus/pages/msg_feed_top/reply_me/view.dart'; import 'package:PiliPlus/pages/msg_feed_top/sys_msg/view.dart'; import 'package:PiliPlus/pages/music/view.dart'; +import 'package:PiliPlus/pages/my_reply/view.dart'; import 'package:PiliPlus/pages/popular_precious/view.dart'; import 'package:PiliPlus/pages/popular_series/view.dart'; import 'package:PiliPlus/pages/search/view.dart'; @@ -190,5 +191,6 @@ class Routes { GetPage(name: '/sameFollowing', page: () => const FollowSamePage()), GetPage(name: '/download', page: () => const DownloadPage()), GetPage(name: '/dlna', page: () => const DLNAPage()), + GetPage(name: '/myReply', page: () => const MyReply()), ]; } diff --git a/lib/utils/page_utils.dart b/lib/utils/page_utils.dart index bd404e869..7fe09b9d9 100644 --- a/lib/utils/page_utils.dart +++ b/lib/utils/page_utils.dart @@ -110,6 +110,7 @@ abstract final class PageUtils { String? id, Object? rid, bool off = false, + Object? type, }) async { assert(id != null || rid != null); SmartDialog.showLoading(); @@ -139,7 +140,7 @@ abstract final class PageUtils { ); } } else { - res.toast(); + SmartDialog.showToast('${type != null ? 'type: $type ' : ''}$res'); } } diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index dc0b77cc3..f4f7c6c2e 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -20,6 +20,7 @@ abstract final class GStorage { static late final Box setting; static late final Box video; static late final Box watchProgress; + static late final Box reply; static Future init() async { await Hive.initFlutter(path.join(appSupportDirPath, 'hive')); @@ -58,6 +59,12 @@ abstract final class GStorage { return deletedEntries > 4; }, ).then((res) => watchProgress = res), + Hive.openBox( + 'reply', + compactionStrategy: (entries, deletedEntries) { + return deletedEntries > 10; + }, + ).then((res) => reply = res), ]); } @@ -100,6 +107,7 @@ abstract final class GStorage { video.compact(), Accounts.account.compact(), watchProgress.compact(), + reply.compact(), ]); } @@ -112,6 +120,7 @@ abstract final class GStorage { video.close(), Accounts.account.close(), watchProgress.close(), + reply.close(), ]); } }