feat: save reply

Closes #614

opt: more panel

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-04-06 11:38:16 +08:00
parent a8daf02610
commit e1ab9e19cb
13 changed files with 926 additions and 450 deletions

View File

@@ -234,197 +234,200 @@ class AuthorPanel extends StatelessWidget {
);
Widget morePanel(context) {
return Container(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
// clipBehavior: Clip.hardEdge,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: Get.back,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28),
),
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline,
borderRadius: const BorderRadius.all(Radius.circular(3))),
return MediaQuery.removePadding(
context: context,
removeLeft: true,
removeRight: true,
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: Get.back,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28),
),
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline,
borderRadius:
const BorderRadius.all(Radius.circular(3))),
),
),
),
),
),
if (item.type == 'DYNAMIC_TYPE_AV')
if (item.type == 'DYNAMIC_TYPE_AV')
ListTile(
onTap: () async {
try {
String bvid = item.modules.moduleDynamic.major.archive.bvid;
var res = await UserHttp.toViewLater(bvid: bvid);
SmartDialog.showToast(res['msg']);
Get.back();
} catch (err) {
SmartDialog.showToast('出错了:${err.toString()}');
}
},
minLeadingWidth: 0,
// dense: true,
leading: const Icon(Icons.watch_later_outlined, size: 19),
title: Text(
'稍后再看',
style: Theme.of(context).textTheme.titleSmall,
),
),
ListTile(
onTap: () async {
try {
String bvid = item.modules.moduleDynamic.major.archive.bvid;
var res = await UserHttp.toViewLater(bvid: bvid);
SmartDialog.showToast(res['msg']);
Get.back();
} catch (err) {
SmartDialog.showToast('出错了:${err.toString()}');
}
},
minLeadingWidth: 0,
// dense: true,
leading: const Icon(Icons.watch_later_outlined, size: 19),
title: Text(
'稍后再看',
'分享动态',
style: Theme.of(context).textTheme.titleSmall,
),
),
ListTile(
title: Text(
'分享动态',
style: Theme.of(context).textTheme.titleSmall,
),
leading: const Icon(Icons.share_outlined, size: 19),
onTap: () {
Get.back();
Utils.shareText(
'${HttpString.dynamicShareBaseUrl}/${item.idStr}');
},
minLeadingWidth: 0,
),
ListTile(
title: Text(
'临时屏蔽:${item.modules.moduleAuthor.name}',
style: Theme.of(context).textTheme.titleSmall,
),
leading: const Icon(Icons.visibility_off_outlined, size: 19),
onTap: () {
Get.back();
DynamicsController dynamicsController =
Get.find<DynamicsController>();
dynamicsController.tempBannedList
.add(item.modules.moduleAuthor.mid);
SmartDialog.showToast(
'已临时屏蔽${item.modules.moduleAuthor.name}(${item.modules.moduleAuthor.mid}),重启恢复');
},
minLeadingWidth: 0,
),
if (item.modules.moduleAuthor.mid == Accounts.main.mid) ...[
ListTile(
leading: const Icon(Icons.share_outlined, size: 19),
onTap: () {
Get.back();
Utils.checkCreatedDyn(id: item.idStr, isManual: true);
Utils.shareText(
'${HttpString.dynamicShareBaseUrl}/${item.idStr}');
},
minLeadingWidth: 0,
leading: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.shield_outlined, size: 19),
const Icon(Icons.published_with_changes_sharp, size: 12),
],
),
title:
Text('检查动态', style: Theme.of(context).textTheme.titleSmall!),
),
if (onRemove != null)
ListTile(
title: Text(
'临时屏蔽:${item.modules.moduleAuthor.name}',
style: Theme.of(context).textTheme.titleSmall,
),
leading: const Icon(Icons.visibility_off_outlined, size: 19),
onTap: () {
Get.back();
DynamicsController dynamicsController =
Get.find<DynamicsController>();
dynamicsController.tempBannedList
.add(item.modules.moduleAuthor.mid);
SmartDialog.showToast(
'已临时屏蔽${item.modules.moduleAuthor.name}(${item.modules.moduleAuthor.mid}),重启恢复');
},
minLeadingWidth: 0,
),
if (item.modules.moduleAuthor.mid == Accounts.main.mid) ...[
ListTile(
onTap: () {
Get.back();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确定删除该动态?'),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
Utils.checkCreatedDyn(id: item.idStr, isManual: true);
},
minLeadingWidth: 0,
leading: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.shield_outlined, size: 19),
const Icon(Icons.published_with_changes_sharp, size: 12),
],
),
title: Text('检查动态',
style: Theme.of(context).textTheme.titleSmall!),
),
if (onRemove != null)
ListTile(
onTap: () {
Get.back();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确定删除该动态?'),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () {
Get.back();
onRemove?.call(item.idStr);
},
child: const Text('确定'),
),
],
),
);
},
minLeadingWidth: 0,
leading: Icon(Icons.delete_outline,
color: Theme.of(context).colorScheme.error, size: 19),
title: Text('删除',
style: Theme.of(context).textTheme.titleSmall!.copyWith(
color: Theme.of(context).colorScheme.error)),
),
],
if (Accounts.main.isLogin)
ListTile(
title: Text(
'举报',
style: Theme.of(context).textTheme.titleSmall!.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
leading: Icon(
Icons.error_outline_outlined,
size: 19,
color: Theme.of(context).colorScheme.error,
),
onTap: () {
Get.back();
autoWrapReportDialog(
context,
ReportOptions.dynamicReport,
(reasonType, reasonDesc, banUid) async {
if (banUid) {
VideoHttp.relationMod(
mid: item.modules.moduleAuthor.mid,
act: 5,
reSrc: 11,
);
}
final res = await Request().post(
'/x/dynamic/feed/dynamic_report/add',
queryParameters: {
'csrf': await Request.getCsrf(),
},
data: {
"accused_uid": item.modules.moduleAuthor.mid,
"dynamic_id": item.idStr,
"reason_type": reasonType,
"reason_desc": reasonType == 0 ? reasonDesc : null,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
TextButton(
onPressed: () {
Get.back();
onRemove?.call(item.idStr);
},
child: const Text('确定'),
),
],
),
);
return res.data as Map;
},
);
},
minLeadingWidth: 0,
leading: Icon(Icons.delete_outline,
color: Theme.of(context).colorScheme.error, size: 19),
title: Text('删除',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(color: Theme.of(context).colorScheme.error)),
),
],
if (Accounts.main.isLogin)
const Divider(thickness: 0.1, height: 1),
ListTile(
title: Text(
'举报',
style: Theme.of(context).textTheme.titleSmall!.copyWith(
color: Theme.of(context).colorScheme.error,
),
),
leading: Icon(
Icons.error_outline_outlined,
size: 19,
color: Theme.of(context).colorScheme.error,
),
onTap: () {
Get.back();
autoWrapReportDialog(
context,
ReportOptions.dynamicReport,
(reasonType, reasonDesc, banUid) async {
if (banUid) {
VideoHttp.relationMod(
mid: item.modules.moduleAuthor.mid,
act: 5,
reSrc: 11,
);
}
final res = await Request().post(
'/x/dynamic/feed/dynamic_report/add',
queryParameters: {
'csrf': await Request.getCsrf(),
},
data: {
"accused_uid": item.modules.moduleAuthor.mid,
"dynamic_id": item.idStr,
"reason_type": reasonType,
"reason_desc": reasonType == 0 ? reasonDesc : null,
},
options: Options(
contentType: Headers.formUrlEncodedContentType,
),
);
return res.data as Map;
},
);
},
onTap: Get.back,
minLeadingWidth: 0,
dense: true,
title: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
textAlign: TextAlign.center,
),
),
const Divider(thickness: 0.1, height: 1),
ListTile(
onTap: Get.back,
minLeadingWidth: 0,
dense: true,
title: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
textAlign: TextAlign.center,
),
),
],
],
),
),
);
}

View File

@@ -158,131 +158,137 @@ class MorePanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
// clipBehavior: Clip.hardEdge,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: () => Get.back(),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28),
),
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline,
borderRadius: const BorderRadius.all(Radius.circular(3))),
return MediaQuery.removePadding(
context: context,
removeLeft: true,
removeRight: true,
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: () => Get.back(),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28),
),
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline,
borderRadius:
const BorderRadius.all(Radius.circular(3))),
),
),
),
),
),
if (item.type == 'DYNAMIC_TYPE_AV')
if (item.type == 'DYNAMIC_TYPE_AV')
ListTile(
onTap: () async {
try {
String bvid = item.modules.moduleDynamic.major.archive.bvid;
var res = await UserHttp.toViewLater(bvid: bvid);
SmartDialog.showToast(res['msg']);
Get.back();
} catch (err) {
SmartDialog.showToast('出错了:${err.toString()}');
}
},
minLeadingWidth: 0,
// dense: true,
leading: const Icon(Icons.watch_later_outlined, size: 19),
title: Text(
'稍后再看',
style: Theme.of(context).textTheme.titleSmall,
),
),
ListTile(
onTap: () async {
try {
String bvid = item.modules.moduleDynamic.major.archive.bvid;
var res = await UserHttp.toViewLater(bvid: bvid);
SmartDialog.showToast(res['msg']);
Get.back();
} catch (err) {
SmartDialog.showToast('出错了:${err.toString()}');
}
},
minLeadingWidth: 0,
// dense: true,
leading: const Icon(Icons.watch_later_outlined, size: 19),
title: Text(
'稍后再看',
'分享动态',
style: Theme.of(context).textTheme.titleSmall,
),
),
ListTile(
title: Text(
'分享动态',
style: Theme.of(context).textTheme.titleSmall,
),
leading: const Icon(Icons.share_outlined, size: 19),
onTap: () {
Get.back();
Utils.shareText(
'${HttpString.dynamicShareBaseUrl}/${item.idStr}');
},
minLeadingWidth: 0,
),
ListTile(
title: Text(
'临时屏蔽:${item.modules.moduleAuthor.name}',
style: Theme.of(context).textTheme.titleSmall,
),
leading: const Icon(Icons.visibility_off_outlined, size: 19),
onTap: () {
Get.back();
DynamicsController dynamicsController =
Get.find<DynamicsController>();
dynamicsController.tempBannedList
.add(item.modules.moduleAuthor.mid);
SmartDialog.showToast(
'已临时屏蔽${item.modules.moduleAuthor.name}(${item.modules.moduleAuthor.mid}),重启恢复');
},
minLeadingWidth: 0,
),
if (item.modules.moduleAuthor.mid == Accounts.main.mid)
ListTile(
onTap: () async {
leading: const Icon(Icons.share_outlined, size: 19),
onTap: () {
Get.back();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确定删除该动态?'),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () {
Get.back();
onRemove?.call(item.idStr);
},
child: const Text('确定'),
),
],
));
Utils.shareText(
'${HttpString.dynamicShareBaseUrl}/${item.idStr}');
},
minLeadingWidth: 0,
leading: Icon(Icons.delete_outline,
color: Theme.of(context).colorScheme.error, size: 19),
title: Text('删除',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(color: Theme.of(context).colorScheme.error)),
),
const Divider(thickness: 0.1, height: 1),
ListTile(
onTap: () => Get.back(),
minLeadingWidth: 0,
dense: true,
title: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
textAlign: TextAlign.center,
ListTile(
title: Text(
'临时屏蔽:${item.modules.moduleAuthor.name}',
style: Theme.of(context).textTheme.titleSmall,
),
leading: const Icon(Icons.visibility_off_outlined, size: 19),
onTap: () {
Get.back();
DynamicsController dynamicsController =
Get.find<DynamicsController>();
dynamicsController.tempBannedList
.add(item.modules.moduleAuthor.mid);
SmartDialog.showToast(
'已临时屏蔽${item.modules.moduleAuthor.name}(${item.modules.moduleAuthor.mid}),重启恢复');
},
minLeadingWidth: 0,
),
),
],
if (item.modules.moduleAuthor.mid == Accounts.main.mid)
ListTile(
onTap: () async {
Get.back();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('确定删除该动态?'),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color:
Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () {
Get.back();
onRemove?.call(item.idStr);
},
child: const Text('确定'),
),
],
));
},
minLeadingWidth: 0,
leading: Icon(Icons.delete_outline,
color: Theme.of(context).colorScheme.error, size: 19),
title: Text('删除',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(color: Theme.of(context).colorScheme.error)),
),
const Divider(thickness: 0.1, height: 1),
ListTile(
onTap: () => Get.back(),
minLeadingWidth: 0,
dense: true,
title: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
textAlign: TextAlign.center,
),
),
],
),
),
);
}

View File

@@ -8,7 +8,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:qr_flutter/qr_flutter.dart';
import 'package:pretty_qr_code/pretty_qr_code.dart';
import 'package:saver_gallery/saver_gallery.dart';
import 'controller.dart';
@@ -52,7 +52,7 @@ class _LoginPageState extends State<LoginPage> {
SmartDialog.showLoading(msg: '正在生成截图');
RenderRepaintBoundary boundary = globalKey.currentContext!
.findRenderObject()! as RenderRepaintBoundary;
var image = await boundary.toImage();
var image = await boundary.toImage(pixelRatio: 3);
ByteData? byteData =
await image.toByteData(format: ImageByteFormat.png);
Uint8List pngBytes = byteData!.buffer.asUint8List();
@@ -93,20 +93,35 @@ class _LoginPageState extends State<LoginPage> {
),
);
}
return QrImageView(
backgroundColor: Colors.white,
eyeStyle: QrEyeStyle(
eyeShape: QrEyeShape.square,
color: Colors.black87,
return Container(
width: 200,
height: 200,
color: Colors.white,
padding: const EdgeInsets.all(8),
child: PrettyQrView.data(
data: _loginPageCtr.codeInfo.value['data']!['url']!,
decoration: PrettyQrDecoration(
shape: PrettyQrRoundedSymbol(
color: Colors.black87,
borderRadius: BorderRadius.circular(0),
),
),
),
dataModuleStyle: QrDataModuleStyle(
dataModuleShape: QrDataModuleShape.square,
color: Colors.black87,
),
data: _loginPageCtr.codeInfo.value['data']!['url']!,
size: 200,
semanticsLabel: '二维码',
);
// return QrImageView(
// backgroundColor: Colors.white,
// eyeStyle: QrEyeStyle(
// eyeShape: QrEyeShape.square,
// color: Colors.black87,
// ),
// dataModuleStyle: QrDataModuleStyle(
// dataModuleShape: QrDataModuleShape.square,
// color: Colors.black87,
// ),
// data: _loginPageCtr.codeInfo.value['data']!['url']!,
// size: 200,
// semanticsLabel: '二维码',
// );
}),
),
const SizedBox(height: 10),

View File

@@ -135,27 +135,32 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
clipBehavior: Clip.hardEdge,
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 10),
...sheetMenu.map(
(item) => ListTile(
onTap: () {
Navigator.pop(context);
menuAction(index, item['id']);
},
minLeadingWidth: 0,
iconColor: Theme.of(context).colorScheme.onSurface,
leading: item['leading'],
title: Text(
item['title'],
style: Theme.of(context).textTheme.titleSmall,
return MediaQuery.removePadding(
context: context,
removeLeft: true,
removeRight: true,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 10),
...sheetMenu.map(
(item) => ListTile(
onTap: () {
Navigator.pop(context);
menuAction(index, item['id']);
},
minLeadingWidth: 0,
iconColor: Theme.of(context).colorScheme.onSurface,
leading: item['leading'],
title: Text(
item['title'],
style: Theme.of(context).textTheme.titleSmall,
),
),
),
),
SizedBox(height: 25 + MediaQuery.paddingOf(context).bottom),
],
SizedBox(height: 25 + MediaQuery.paddingOf(context).bottom),
],
),
);
},
);

View File

@@ -1140,12 +1140,8 @@ class ReplyItem extends StatelessWidget {
Color errorColor = Theme.of(context).colorScheme.error;
return Padding(
padding: EdgeInsets.only(
bottom: MediaQueryData.fromView(
WidgetsBinding.instance.platformDispatcher.views.single)
.padding
.bottom +
20),
padding:
EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom + 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [

View File

@@ -8,6 +8,7 @@ import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/video/detail/reply/widgets/reply_save.dart';
import 'package:PiliPlus/pages/video/detail/reply/widgets/zan_grpc.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/global_data.dart';
@@ -43,8 +44,8 @@ class ReplyItemGrpc extends StatelessWidget {
this.onViewImage,
this.onDismissed,
this.callback,
required this.onCheckReply,
required this.onToggleTop,
this.onCheckReply,
this.onToggleTop,
});
final ReplyInfo replyItem;
final String? replyLevel;
@@ -60,8 +61,8 @@ class ReplyItemGrpc extends StatelessWidget {
final VoidCallback? onViewImage;
final ValueChanged<int>? onDismissed;
final Function(List<String>, int)? callback;
final ValueChanged<ReplyInfo> onCheckReply;
final Function(bool isUpTop, int rpid) onToggleTop;
final ValueChanged<ReplyInfo>? onCheckReply;
final Function(bool isUpTop, int rpid)? onToggleTop;
@override
Widget build(BuildContext context) {
@@ -277,7 +278,6 @@ class ReplyItemGrpc extends StatelessWidget {
},
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
lfAvtar(context),
const SizedBox(width: 12),
@@ -344,8 +344,12 @@ class ReplyItemGrpc extends StatelessWidget {
),
// title
Padding(
padding:
const EdgeInsets.only(top: 10, left: 45, right: 6, bottom: 4),
padding: EdgeInsets.only(
top: 10,
left: replyLevel == '' ? 6 : 45,
right: 6,
bottom: 4,
),
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
String text = replyItem.content.message;
@@ -400,12 +404,12 @@ class ReplyItemGrpc extends StatelessWidget {
),
),
// 操作区域
buttonAction(context, replyItem.replyControl),
if (replyLevel != '') buttonAction(context, replyItem.replyControl),
// 一楼的评论
if (( //replyItem.replyControl!.isShow! ||
if (showReplyRow &&
( //replyItem.replyControl!.isShow! ||
replyItem.replies.isNotEmpty ||
replyItem.replyControl.subReplyEntryText.isNotEmpty) &&
showReplyRow) ...[
replyItem.replyControl.subReplyEntryText.isNotEmpty)) ...[
Padding(
padding: const EdgeInsets.only(top: 5, bottom: 12),
child: replyItemRow(
@@ -1195,11 +1199,31 @@ class ReplyItemGrpc extends StatelessWidget {
break;
case 'checkReply':
Get.back();
onCheckReply(item);
onCheckReply?.call(item);
break;
case 'top':
Get.back();
onToggleTop(item.replyControl.isUpTop, item.id.toInt());
onToggleTop?.call(item.replyControl.isUpTop, item.id.toInt());
break;
case 'saveReply':
Get.back();
Get.generalDialog(
barrierLabel: '',
barrierDismissible: true,
pageBuilder: (context, animation, secondaryAnimation) {
return ReplySavePanel(replyItem: item);
},
transitionDuration: const Duration(milliseconds: 255),
transitionBuilder: (context, animation, secondaryAnimation, child) {
var tween = Tween<double>(begin: 0, end: 1)
.chain(CurveTween(curve: Curves.easeInOut));
return FadeTransition(
opacity: animation.drive(tween),
child: child,
);
},
routeSettings: RouteSettings(arguments: Get.arguments),
);
break;
default:
}
@@ -1207,93 +1231,108 @@ class ReplyItemGrpc extends StatelessWidget {
Color errorColor = Theme.of(context).colorScheme.error;
return Padding(
padding: EdgeInsets.only(
bottom: MediaQueryData.fromView(
WidgetsBinding.instance.platformDispatcher.views.single)
.padding
.bottom +
20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: Get.back,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28),
),
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline,
borderRadius: const BorderRadius.all(Radius.circular(3))),
return MediaQuery.removePadding(
context: context,
removeLeft: true,
removeRight: true,
child: Padding(
padding:
EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom + 20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
InkWell(
onTap: Get.back,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(28),
topRight: Radius.circular(28),
),
child: Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline,
borderRadius:
const BorderRadius.all(Radius.circular(3))),
),
),
),
),
),
if (ownerMid == upMid.toInt() || ownerMid == item.member.mid.toInt())
ListTile(
onTap: () => menuActionHandler('delete'),
minLeadingWidth: 0,
leading: Icon(Icons.delete_outlined, color: errorColor, size: 19),
title: Text('删除',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(color: errorColor)),
),
if (ownerMid != 0)
ListTile(
onTap: () => menuActionHandler('report'),
minLeadingWidth: 0,
leading: Icon(Icons.error_outline, color: errorColor, size: 19),
title: Text('举报',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(color: errorColor)),
),
if (replyLevel == '1' && isSubReply.not && ownerMid == upMid.toInt())
ListTile(
onTap: () => menuActionHandler('top'),
minLeadingWidth: 0,
leading: Icon(Icons.vertical_align_top, size: 19),
title: Text('${replyItem.replyControl.isUpTop ? '取消' : ''}置顶',
style: Theme.of(context).textTheme.titleSmall!),
),
ListTile(
onTap: () => menuActionHandler('copyAll'),
minLeadingWidth: 0,
leading: const Icon(Icons.copy_all_outlined, size: 19),
title: Text('复制全部', style: Theme.of(context).textTheme.titleSmall),
),
ListTile(
onTap: () => menuActionHandler('copyFreedom'),
minLeadingWidth: 0,
leading: const Icon(Icons.copy_outlined, size: 19),
title: Text('自由复制', style: Theme.of(context).textTheme.titleSmall),
),
if (item.mid.toInt() == ownerMid)
ListTile(
onTap: () => menuActionHandler('checkReply'),
minLeadingWidth: 0,
leading: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.shield_outlined, size: 19),
const Icon(Icons.reply, size: 12),
],
if (ownerMid == upMid.toInt() ||
ownerMid == item.member.mid.toInt())
ListTile(
onTap: () => menuActionHandler('delete'),
minLeadingWidth: 0,
leading:
Icon(Icons.delete_outlined, color: errorColor, size: 19),
title: Text('删除',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(color: errorColor)),
),
if (ownerMid != 0)
ListTile(
onTap: () => menuActionHandler('report'),
minLeadingWidth: 0,
leading: Icon(Icons.error_outline, color: errorColor, size: 19),
title: Text('举报',
style: Theme.of(context)
.textTheme
.titleSmall!
.copyWith(color: errorColor)),
),
if (replyLevel == '1' &&
isSubReply.not &&
ownerMid == upMid.toInt())
ListTile(
onTap: () => menuActionHandler('top'),
minLeadingWidth: 0,
leading: Icon(Icons.vertical_align_top, size: 19),
title: Text('${replyItem.replyControl.isUpTop ? '取消' : ''}置顶',
style: Theme.of(context).textTheme.titleSmall!),
),
ListTile(
onTap: () => menuActionHandler('copyAll'),
minLeadingWidth: 0,
leading: const Icon(Icons.copy_all_outlined, size: 19),
title:
Text('检查评论', style: Theme.of(context).textTheme.titleSmall),
Text('复制全部', style: Theme.of(context).textTheme.titleSmall),
),
],
ListTile(
onTap: () => menuActionHandler('copyFreedom'),
minLeadingWidth: 0,
leading: const Icon(Icons.copy_outlined, size: 19),
title:
Text('自由复制', style: Theme.of(context).textTheme.titleSmall),
),
ListTile(
onTap: () => menuActionHandler('saveReply'),
minLeadingWidth: 0,
leading: const Icon(Icons.save_alt, size: 19),
title:
Text('保存评论', style: Theme.of(context).textTheme.titleSmall),
),
if (item.mid.toInt() == ownerMid)
ListTile(
onTap: () => menuActionHandler('checkReply'),
minLeadingWidth: 0,
leading: Stack(
alignment: Alignment.center,
children: [
const Icon(Icons.shield_outlined, size: 19),
const Icon(Icons.reply, size: 12),
],
),
title:
Text('检查评论', style: Theme.of(context).textTheme.titleSmall),
),
],
),
),
);
}

View File

@@ -0,0 +1,385 @@
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui';
import 'package:PiliPlus/common/widgets/icon_button.dart';
import 'package:PiliPlus/common/widgets/network_img_layer.dart';
import 'package:PiliPlus/grpc/app/main/community/reply/v1/reply.pb.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/video/detail/introduction/controller.dart';
import 'package:PiliPlus/pages/video/detail/reply/widgets/reply_item_grpc.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pretty_qr_code/pretty_qr_code.dart';
import 'package:saver_gallery/saver_gallery.dart';
import 'package:share_plus/share_plus.dart';
class ReplySavePanel extends StatefulWidget {
const ReplySavePanel({required this.replyItem, super.key});
final ReplyInfo replyItem;
@override
State<ReplySavePanel> createState() => _ReplySavePanelState();
}
class _ReplySavePanelState extends State<ReplySavePanel> {
final boundaryKey = GlobalKey();
void _onSaveOrSharePic([bool isShare = false]) async {
SmartDialog.showLoading();
try {
RenderRepaintBoundary boundary = boundaryKey.currentContext!
.findRenderObject() as RenderRepaintBoundary;
var image = await boundary.toImage(pixelRatio: 3);
ByteData? byteData = await image.toByteData(format: ImageByteFormat.png);
Uint8List pngBytes = byteData!.buffer.asUint8List();
String picName =
"plpl_reply_${DateTime.now().toString().replaceAll(RegExp(r'[- :]'), '').split('.').first}";
if (isShare) {
Get.back();
SmartDialog.dismiss();
Share.shareXFiles(
[
XFile.fromData(
pngBytes,
name: picName,
mimeType: 'image/png',
)
],
sharePositionOrigin: await Utils.isIpad()
? Rect.fromLTWH(0, 0, Get.width, Get.height / 2)
: null,
);
} else {
final result = await SaverGallery.saveImage(
pngBytes,
fileName: '$picName.png',
androidRelativePath: "Pictures/PiliPlus",
skipIfExists: false,
);
SmartDialog.dismiss();
if (result.isSuccess) {
Get.back();
SmartDialog.showToast('保存成功');
} else if (result.errorMessage?.isNotEmpty == true) {
SmartDialog.showToast(result.errorMessage!);
}
}
} catch (e) {
debugPrint('on save/share reply: $e');
SmartDialog.dismiss();
}
}
@override
Widget build(BuildContext context) {
String? cover;
String? title;
int? pubdate;
String? uname;
String uri = '';
final currentRoute = Get.currentRoute;
late final hasRoot = widget.replyItem.hasRoot();
if (currentRoute.startsWith('/video')) {
try {
final heroTag = Get.arguments?['heroTag'];
late final ctr = Get.find<VideoIntroController>(tag: heroTag);
cover = ctr.videoDetail.value.pic;
title = ctr.videoDetail.value.title;
pubdate = ctr.videoDetail.value.pubdate;
uname = ctr.videoDetail.value.owner?.name;
} catch (_) {}
uri =
'bilibili://video/${widget.replyItem.oid}?comment_root_id=${hasRoot ? widget.replyItem.root : widget.replyItem.id}${hasRoot ? '&comment_secondary_id=${widget.replyItem.id}' : ''}';
} else if (currentRoute.startsWith('/dynamicDetail')) {
try {
DynamicItemModel item = Get.arguments['item'];
uname = item.modules?.moduleAuthor?.name;
final type = widget.replyItem.type.toInt();
late final oid = item.idStr;
late final rootId =
hasRoot ? widget.replyItem.root : widget.replyItem.id;
late final anchor = hasRoot ? 'anchor=${widget.replyItem.id}&' : '';
late final enterUri = 'bilibili://following/detail/$oid';
uri = switch (type) {
1 =>
'bilibili://video/${item.basic!['rid_str']}?comment_root_id=${hasRoot ? widget.replyItem.root : widget.replyItem.id}${hasRoot ? '&comment_secondary_id=${widget.replyItem.id}' : ''}',
11 ||
12 =>
'bilibili://comment/detail/$type/${item.basic!['rid_str']}/$rootId/?${anchor}enterUri=$enterUri',
_ =>
'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=$enterUri',
};
} catch (_) {}
} else if (currentRoute.startsWith('/Scaffold')) {
try {
final type = widget.replyItem.type.toInt();
late final oid = Get.arguments['oid'];
late final rootId =
hasRoot ? widget.replyItem.root : widget.replyItem.id;
late final anchor = hasRoot ? 'anchor=${widget.replyItem.id}&' : '';
late final enterUri = 'bilibili://following/detail/$oid';
uri = switch (type) {
1 =>
'bilibili://video/$oid?comment_root_id=${hasRoot ? widget.replyItem.root : widget.replyItem.id}${hasRoot ? '&comment_secondary_id=${widget.replyItem.id}' : ''}',
11 ||
12 =>
'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=${Get.arguments['enterUri']}',
_ =>
'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=$enterUri',
};
} catch (_) {}
} else if (currentRoute.startsWith('/htmlRender')) {
try {
final type = widget.replyItem.type.toInt();
late final oid = widget.replyItem.oid;
late final rootId =
hasRoot ? widget.replyItem.root : widget.replyItem.id;
late final anchor = hasRoot ? 'anchor=${widget.replyItem.id}&' : '';
late final enterUri =
'bilibili://following/detail/${Get.parameters['id']}';
uri =
'bilibili://comment/detail/$type/$oid/$rootId/?${anchor}enterUri=$enterUri';
} catch (_) {}
}
debugPrint(uri);
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
Get.back();
},
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
SingleChildScrollView(
padding: const EdgeInsets.only(top: 12, bottom: 80),
child: SafeArea(
child: GestureDetector(
onTap: () {},
child: Container(
width: min(Get.width, Get.height),
margin: const EdgeInsets.symmetric(horizontal: 12),
child: RepaintBoundary(
key: boundaryKey,
child: Container(
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(12),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
IgnorePointer(
child: ReplyItemGrpc(
replyItem: widget.replyItem,
showReplyRow: false,
replyLevel: '',
needDivider: false,
),
),
if (cover?.isNotEmpty == true &&
title?.isNotEmpty == true)
Container(
height: 81,
clipBehavior: Clip.hardEdge,
margin:
const EdgeInsets.symmetric(horizontal: 12),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.onInverseSurface,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
NetworkImgLayer(
radius: 6,
src: cover!,
height: MediaQuery.textScalerOf(context)
.scale(65),
width: MediaQuery.textScalerOf(context)
.scale(65) *
16 /
9,
quality: 100,
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'$title\n',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (pubdate != null) ...[
const Spacer(),
Text(
Utils.dateFormat(
pubdate,
formatType: 'detail',
),
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline,
),
),
],
],
),
),
],
),
),
Row(
children: [
Image.asset(
'assets/images/logo/logo_2.png',
width: 100,
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
if (uri.isNotEmpty) ...[
Expanded(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.end,
children: [
if (uname?.isNotEmpty == true) ...[
Text(
'@$uname',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.primary,
),
),
const SizedBox(height: 4),
],
Text(
'识别二维码,查看评论',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant,
),
),
const SizedBox(height: 4),
Text(
DateTime.now()
.toString()
.split('.')
.first,
style: TextStyle(
fontSize: 13,
color: Theme.of(context)
.colorScheme
.outline,
),
),
],
),
),
Container(
width: 100,
height: 100,
padding: const EdgeInsets.all(12),
child: Container(
color: Get.isDarkMode
? Colors.white
: Theme.of(context).colorScheme.surface,
padding: const EdgeInsets.all(3),
child: PrettyQrView.data(
data: uri,
decoration: const PrettyQrDecoration(
shape: PrettyQrRoundedSymbol(
borderRadius: BorderRadius.zero,
),
),
),
),
),
],
],
),
],
),
),
),
),
),
),
),
Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: [
Colors.black87,
Colors.transparent,
],
),
),
padding: const EdgeInsets.only(bottom: 25, top: 10),
child: SafeArea(
top: false,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
iconButton(
size: 42,
tooltip: '关闭',
context: context,
icon: Icons.clear,
onPressed: Get.back,
bgColor: Theme.of(context).colorScheme.onInverseSurface,
iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
),
const SizedBox(width: 40),
iconButton(
size: 42,
tooltip: '分享',
context: context,
icon: Icons.share,
onPressed: () => _onSaveOrSharePic(true),
),
const SizedBox(width: 40),
iconButton(
size: 42,
tooltip: '保存',
context: context,
icon: Icons.save_alt,
onPressed: () => _onSaveOrSharePic(),
),
],
),
),
),
),
],
),
);
}
}

View File

@@ -67,10 +67,8 @@ class VideoReplyReplyController extends ReplyController
firstFloor = replies.root;
}
if (id != null) {
index = replies.root.replies
.map((item) => item.id.toInt())
.toList()
.indexOf(id!);
index =
replies.root.replies.indexWhere((item) => item.id.toInt() == id);
if (index == -1) {
index = null;
} else {

View File

@@ -110,6 +110,12 @@ class PiliScheme {
int? rpid = int.tryParse(queryParameters['comment_root_id']!);
if (oid != null && rpid != null) {
Get.to(
arguments: {
'oid': oid,
'rpid': rpid,
'type': ReplyType.video.index,
'id': queryParameters['comment_secondary_id'],
},
() => Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
@@ -240,16 +246,24 @@ class PiliScheme {
if (path.startsWith("/detail/")) {
// bilibili://comment/detail/17/832703053858603029/238686570016/?subType=0&anchor=238686628816&showEnter=1&extraIntentId=0&scene=1&enterName=%E6%9F%A5%E7%9C%8B%E5%8A%A8%E6%80%81%E8%AF%A6%E6%83%85&enterUri=bilibili://following/detail/832703053858603029
List<String> pathSegments = uri.pathSegments;
Map<String, String> queryParameters = uri.queryParameters;
int type = int.parse(pathSegments[1]); // business_id
int oid = int.parse(pathSegments[2]); // subject_id
int rootId = int.parse(pathSegments[3]); // root_id // target_id
int? rpId = uri.queryParameters['anchor'] != null // source_id
? int.tryParse(uri.queryParameters['anchor']!)
int? rpId = queryParameters['anchor'] != null // source_id
? int.tryParse(queryParameters['anchor']!)
: null;
// int subType = int.parse(value.queryParameters['subType'] ?? '0');
// int subType = int.parse(queryParameters['subType'] ?? '0');
// int extraIntentId =
// int.parse(value.queryParameters['extraIntentId'] ?? '0');
// int.parse(queryParameters['extraIntentId'] ?? '0');
Get.to(
arguments: {
'oid': oid,
'rpid': rootId,
'id': rpId,
'type': type,
'enterUri': queryParameters['enterUri'],
},
() => Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
@@ -258,7 +272,7 @@ class PiliScheme {
IconButton(
tooltip: '前往',
onPressed: () {
String? enterUri = uri.queryParameters['enterUri'];
String? enterUri = queryParameters['enterUri'];
if (enterUri != null) {
routePush(Uri.parse(enterUri));
} else {
@@ -289,6 +303,11 @@ class PiliScheme {
int oid = int.parse(pathSegments[2]); // subject_id
int rpId = int.parse(pathSegments[3]); // source_id
Get.to(
arguments: {
'oid': oid,
'rpid': rpId,
'type': type,
},
() => Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
@@ -328,6 +347,22 @@ class PiliScheme {
// businessId == 17 => dynId == oid
// bilibili://following/detail/832703053858603029 (dynId)
// bilibili://following/detail/12345678?comment_root_id=654321\u0026comment_on=1
String? cvid = RegExp(r'^/detail/cv(\d+)', caseSensitive: false)
.firstMatch(path)
?.group(1);
if (cvid != null) {
Utils.toDupNamed(
'/htmlRender',
parameters: {
'url': 'https://www.bilibili.com/read/cv$cvid',
'title': '',
'id': 'cv$cvid',
'dynamicType': 'read'
},
off: off,
);
return true;
}
if ((oid != null || businessId == 17) &&
path.startsWith("/detail/")) {
final queryParameters = uri.queryParameters;
@@ -337,6 +372,12 @@ class PiliScheme {
int? rpid = int.tryParse(commentRootId);
if (dynId != null && rpid != null) {
Get.to(
arguments: {
'oid': oid ?? dynId,
'rpid': rpid,
'type': businessId ?? ReplyType.dynamics.index,
'id': queryParameters['comment_secondary_id'],
},
() => Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(

View File

@@ -29,7 +29,7 @@ class DownloadUtils {
File(path).writeAsBytesSync(response.data);
Rect? sharePositionOrigin;
if (Platform.isIOS && (await Utils.isIpad())) {
if (await Utils.isIpad()) {
sharePositionOrigin = Rect.fromLTWH(0, 0, Get.width, Get.height / 2);
}

View File

@@ -408,7 +408,7 @@ class Utils {
RegExp(r'(@(\d+[a-z]_?)*)(\..*)?$', caseSensitive: false);
static String thumbnailImgUrl(String? src, [int? quality]) {
if (src != null) {
if (src != null && quality != 100) {
bool hasMatch = false;
src = src.splitMapJoin(
regExp,
@@ -430,7 +430,10 @@ class Utils {
static bool? _isIpad;
static Future<bool> isIpad() async {
static FutureOr<bool> isIpad() async {
if (Platform.isIOS.not) {
return false;
}
if (_isIpad != null) {
return _isIpad!;
}
@@ -443,7 +446,7 @@ class Utils {
static void shareText(String text) async {
try {
Rect? sharePositionOrigin;
if (Platform.isIOS && (await isIpad())) {
if (await isIpad()) {
sharePositionOrigin = Rect.fromLTWH(0, 0, Get.width, Get.height / 2);
}
Share.share(
@@ -1866,22 +1869,6 @@ class Utils {
}
}
static double getSheetHeight(BuildContext context) {
double height = context.height.abs();
double width = context.width.abs();
if (height > width) {
//return height * 0.7;
double paddingTop = MediaQueryData.fromView(
WidgetsBinding.instance.platformDispatcher.views.single)
.padding
.top;
paddingTop += width * 9 / 16;
return height - paddingTop;
}
//横屏状态
return height;
}
static void appSign(Map<String, dynamic> params,
[String appkey = Constants.appKey, String appsec = Constants.appSec]) {
params['appkey'] = appkey;

View File

@@ -1440,6 +1440,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.1"
pretty_qr_code:
dependency: "direct main"
description:
name: pretty_qr_code
sha256: cbdb4af29da1c1fa21dd76f809646c591320ab9e435d3b0eab867492d43607d5
url: "https://pub.dev"
source: hosted
version: "3.3.0"
protobuf:
dependency: "direct main"
description:
@@ -1472,14 +1480,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.2"
qr_flutter:
dependency: "direct main"
description:
name: qr_flutter
sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
rxdart:
dependency: "direct overridden"
description:

View File

@@ -54,7 +54,8 @@ dependencies:
saver_gallery: ^4.0.1
# QRCode
qr_flutter: ^4.1.0
# qr_flutter: ^4.1.0
pretty_qr_code: ^3.3.0
# 存储
path_provider: ^2.1.5