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,9 +234,12 @@ class AuthorPanel extends StatelessWidget {
); );
Widget morePanel(context) { Widget morePanel(context) {
return Container( return MediaQuery.removePadding(
context: context,
removeLeft: true,
removeRight: true,
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
// clipBehavior: Clip.hardEdge,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -255,7 +258,8 @@ class AuthorPanel extends StatelessWidget {
height: 3, height: 3,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
borderRadius: const BorderRadius.all(Radius.circular(3))), borderRadius:
const BorderRadius.all(Radius.circular(3))),
), ),
), ),
), ),
@@ -324,8 +328,8 @@ class AuthorPanel extends StatelessWidget {
const Icon(Icons.published_with_changes_sharp, size: 12), const Icon(Icons.published_with_changes_sharp, size: 12),
], ],
), ),
title: title: Text('检查动态',
Text('检查动态', style: Theme.of(context).textTheme.titleSmall!), style: Theme.of(context).textTheme.titleSmall!),
), ),
if (onRemove != null) if (onRemove != null)
ListTile( ListTile(
@@ -360,10 +364,8 @@ class AuthorPanel extends StatelessWidget {
leading: Icon(Icons.delete_outline, leading: Icon(Icons.delete_outline,
color: Theme.of(context).colorScheme.error, size: 19), color: Theme.of(context).colorScheme.error, size: 19),
title: Text('删除', title: Text('删除',
style: Theme.of(context) style: Theme.of(context).textTheme.titleSmall!.copyWith(
.textTheme color: Theme.of(context).colorScheme.error)),
.titleSmall!
.copyWith(color: Theme.of(context).colorScheme.error)),
), ),
], ],
if (Accounts.main.isLogin) if (Accounts.main.isLogin)
@@ -426,6 +428,7 @@ class AuthorPanel extends StatelessWidget {
), ),
], ],
), ),
),
); );
} }
} }

View File

@@ -158,9 +158,12 @@ class MorePanel extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return MediaQuery.removePadding(
context: context,
removeLeft: true,
removeRight: true,
child: Padding(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom), padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
// clipBehavior: Clip.hardEdge,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -179,7 +182,8 @@ class MorePanel extends StatelessWidget {
height: 3, height: 3,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
borderRadius: const BorderRadius.all(Radius.circular(3))), borderRadius:
const BorderRadius.all(Radius.circular(3))),
), ),
), ),
), ),
@@ -248,7 +252,8 @@ class MorePanel extends StatelessWidget {
child: Text( child: Text(
'取消', '取消',
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.outline, color:
Theme.of(context).colorScheme.outline,
), ),
), ),
), ),
@@ -284,6 +289,7 @@ class MorePanel extends StatelessWidget {
), ),
], ],
), ),
),
); );
} }
} }

View File

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

View File

@@ -135,7 +135,11 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
backgroundColor: Theme.of(context).colorScheme.surface, backgroundColor: Theme.of(context).colorScheme.surface,
builder: (context) { builder: (context) {
return Column( return MediaQuery.removePadding(
context: context,
removeLeft: true,
removeRight: true,
child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
const SizedBox(height: 10), const SizedBox(height: 10),
@@ -156,6 +160,7 @@ class _PlaySpeedPageState extends State<PlaySpeedPage> {
), ),
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; Color errorColor = Theme.of(context).colorScheme.error;
return Padding( return Padding(
padding: EdgeInsets.only( padding:
bottom: MediaQueryData.fromView( EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom + 20),
WidgetsBinding.instance.platformDispatcher.views.single)
.padding
.bottom +
20),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ 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/init.dart';
import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/dynamics/result.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/pages/video/detail/reply/widgets/zan_grpc.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/global_data.dart';
@@ -43,8 +44,8 @@ class ReplyItemGrpc extends StatelessWidget {
this.onViewImage, this.onViewImage,
this.onDismissed, this.onDismissed,
this.callback, this.callback,
required this.onCheckReply, this.onCheckReply,
required this.onToggleTop, this.onToggleTop,
}); });
final ReplyInfo replyItem; final ReplyInfo replyItem;
final String? replyLevel; final String? replyLevel;
@@ -60,8 +61,8 @@ class ReplyItemGrpc extends StatelessWidget {
final VoidCallback? onViewImage; final VoidCallback? onViewImage;
final ValueChanged<int>? onDismissed; final ValueChanged<int>? onDismissed;
final Function(List<String>, int)? callback; final Function(List<String>, int)? callback;
final ValueChanged<ReplyInfo> onCheckReply; final ValueChanged<ReplyInfo>? onCheckReply;
final Function(bool isUpTop, int rpid) onToggleTop; final Function(bool isUpTop, int rpid)? onToggleTop;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -277,7 +278,6 @@ class ReplyItemGrpc extends StatelessWidget {
}, },
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
lfAvtar(context), lfAvtar(context),
const SizedBox(width: 12), const SizedBox(width: 12),
@@ -344,8 +344,12 @@ class ReplyItemGrpc extends StatelessWidget {
), ),
// title // title
Padding( Padding(
padding: padding: EdgeInsets.only(
const EdgeInsets.only(top: 10, left: 45, right: 6, bottom: 4), top: 10,
left: replyLevel == '' ? 6 : 45,
right: 6,
bottom: 4,
),
child: LayoutBuilder( child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) { builder: (BuildContext context, BoxConstraints constraints) {
String text = replyItem.content.message; 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.replies.isNotEmpty ||
replyItem.replyControl.subReplyEntryText.isNotEmpty) && replyItem.replyControl.subReplyEntryText.isNotEmpty)) ...[
showReplyRow) ...[
Padding( Padding(
padding: const EdgeInsets.only(top: 5, bottom: 12), padding: const EdgeInsets.only(top: 5, bottom: 12),
child: replyItemRow( child: replyItemRow(
@@ -1195,11 +1199,31 @@ class ReplyItemGrpc extends StatelessWidget {
break; break;
case 'checkReply': case 'checkReply':
Get.back(); Get.back();
onCheckReply(item); onCheckReply?.call(item);
break; break;
case 'top': case 'top':
Get.back(); 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; break;
default: default:
} }
@@ -1207,13 +1231,13 @@ class ReplyItemGrpc extends StatelessWidget {
Color errorColor = Theme.of(context).colorScheme.error; Color errorColor = Theme.of(context).colorScheme.error;
return Padding( return MediaQuery.removePadding(
padding: EdgeInsets.only( context: context,
bottom: MediaQueryData.fromView( removeLeft: true,
WidgetsBinding.instance.platformDispatcher.views.single) removeRight: true,
.padding child: Padding(
.bottom + padding:
20), EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom + 20),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -1232,16 +1256,19 @@ class ReplyItemGrpc extends StatelessWidget {
height: 3, height: 3,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.outline, color: Theme.of(context).colorScheme.outline,
borderRadius: const BorderRadius.all(Radius.circular(3))), borderRadius:
const BorderRadius.all(Radius.circular(3))),
), ),
), ),
), ),
), ),
if (ownerMid == upMid.toInt() || ownerMid == item.member.mid.toInt()) if (ownerMid == upMid.toInt() ||
ownerMid == item.member.mid.toInt())
ListTile( ListTile(
onTap: () => menuActionHandler('delete'), onTap: () => menuActionHandler('delete'),
minLeadingWidth: 0, minLeadingWidth: 0,
leading: Icon(Icons.delete_outlined, color: errorColor, size: 19), leading:
Icon(Icons.delete_outlined, color: errorColor, size: 19),
title: Text('删除', title: Text('删除',
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
@@ -1259,7 +1286,9 @@ class ReplyItemGrpc extends StatelessWidget {
.titleSmall! .titleSmall!
.copyWith(color: errorColor)), .copyWith(color: errorColor)),
), ),
if (replyLevel == '1' && isSubReply.not && ownerMid == upMid.toInt()) if (replyLevel == '1' &&
isSubReply.not &&
ownerMid == upMid.toInt())
ListTile( ListTile(
onTap: () => menuActionHandler('top'), onTap: () => menuActionHandler('top'),
minLeadingWidth: 0, minLeadingWidth: 0,
@@ -1271,13 +1300,22 @@ class ReplyItemGrpc extends StatelessWidget {
onTap: () => menuActionHandler('copyAll'), onTap: () => menuActionHandler('copyAll'),
minLeadingWidth: 0, minLeadingWidth: 0,
leading: const Icon(Icons.copy_all_outlined, size: 19), leading: const Icon(Icons.copy_all_outlined, size: 19),
title: Text('复制全部', style: Theme.of(context).textTheme.titleSmall), title:
Text('复制全部', style: Theme.of(context).textTheme.titleSmall),
), ),
ListTile( ListTile(
onTap: () => menuActionHandler('copyFreedom'), onTap: () => menuActionHandler('copyFreedom'),
minLeadingWidth: 0, minLeadingWidth: 0,
leading: const Icon(Icons.copy_outlined, size: 19), leading: const Icon(Icons.copy_outlined, size: 19),
title: Text('自由复制', style: Theme.of(context).textTheme.titleSmall), 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) if (item.mid.toInt() == ownerMid)
ListTile( ListTile(
@@ -1295,6 +1333,7 @@ class ReplyItemGrpc extends StatelessWidget {
), ),
], ],
), ),
),
); );
} }
} }

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; firstFloor = replies.root;
} }
if (id != null) { if (id != null) {
index = replies.root.replies index =
.map((item) => item.id.toInt()) replies.root.replies.indexWhere((item) => item.id.toInt() == id);
.toList()
.indexOf(id!);
if (index == -1) { if (index == -1) {
index = null; index = null;
} else { } else {

View File

@@ -110,6 +110,12 @@ class PiliScheme {
int? rpid = int.tryParse(queryParameters['comment_root_id']!); int? rpid = int.tryParse(queryParameters['comment_root_id']!);
if (oid != null && rpid != null) { if (oid != null && rpid != null) {
Get.to( Get.to(
arguments: {
'oid': oid,
'rpid': rpid,
'type': ReplyType.video.index,
'id': queryParameters['comment_secondary_id'],
},
() => Scaffold( () => Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(
@@ -240,16 +246,24 @@ class PiliScheme {
if (path.startsWith("/detail/")) { 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 // 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; List<String> pathSegments = uri.pathSegments;
Map<String, String> queryParameters = uri.queryParameters;
int type = int.parse(pathSegments[1]); // business_id int type = int.parse(pathSegments[1]); // business_id
int oid = int.parse(pathSegments[2]); // subject_id int oid = int.parse(pathSegments[2]); // subject_id
int rootId = int.parse(pathSegments[3]); // root_id // target_id int rootId = int.parse(pathSegments[3]); // root_id // target_id
int? rpId = uri.queryParameters['anchor'] != null // source_id int? rpId = queryParameters['anchor'] != null // source_id
? int.tryParse(uri.queryParameters['anchor']!) ? int.tryParse(queryParameters['anchor']!)
: null; : null;
// int subType = int.parse(value.queryParameters['subType'] ?? '0'); // int subType = int.parse(queryParameters['subType'] ?? '0');
// int extraIntentId = // int extraIntentId =
// int.parse(value.queryParameters['extraIntentId'] ?? '0'); // int.parse(queryParameters['extraIntentId'] ?? '0');
Get.to( Get.to(
arguments: {
'oid': oid,
'rpid': rootId,
'id': rpId,
'type': type,
'enterUri': queryParameters['enterUri'],
},
() => Scaffold( () => Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(
@@ -258,7 +272,7 @@ class PiliScheme {
IconButton( IconButton(
tooltip: '前往', tooltip: '前往',
onPressed: () { onPressed: () {
String? enterUri = uri.queryParameters['enterUri']; String? enterUri = queryParameters['enterUri'];
if (enterUri != null) { if (enterUri != null) {
routePush(Uri.parse(enterUri)); routePush(Uri.parse(enterUri));
} else { } else {
@@ -289,6 +303,11 @@ class PiliScheme {
int oid = int.parse(pathSegments[2]); // subject_id int oid = int.parse(pathSegments[2]); // subject_id
int rpId = int.parse(pathSegments[3]); // source_id int rpId = int.parse(pathSegments[3]); // source_id
Get.to( Get.to(
arguments: {
'oid': oid,
'rpid': rpId,
'type': type,
},
() => Scaffold( () => Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(
@@ -328,6 +347,22 @@ class PiliScheme {
// businessId == 17 => dynId == oid // businessId == 17 => dynId == oid
// bilibili://following/detail/832703053858603029 (dynId) // bilibili://following/detail/832703053858603029 (dynId)
// bilibili://following/detail/12345678?comment_root_id=654321\u0026comment_on=1 // 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) && if ((oid != null || businessId == 17) &&
path.startsWith("/detail/")) { path.startsWith("/detail/")) {
final queryParameters = uri.queryParameters; final queryParameters = uri.queryParameters;
@@ -337,6 +372,12 @@ class PiliScheme {
int? rpid = int.tryParse(commentRootId); int? rpid = int.tryParse(commentRootId);
if (dynId != null && rpid != null) { if (dynId != null && rpid != null) {
Get.to( Get.to(
arguments: {
'oid': oid ?? dynId,
'rpid': rpid,
'type': businessId ?? ReplyType.dynamics.index,
'id': queryParameters['comment_secondary_id'],
},
() => Scaffold( () => Scaffold(
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: AppBar( appBar: AppBar(

View File

@@ -29,7 +29,7 @@ class DownloadUtils {
File(path).writeAsBytesSync(response.data); File(path).writeAsBytesSync(response.data);
Rect? sharePositionOrigin; Rect? sharePositionOrigin;
if (Platform.isIOS && (await Utils.isIpad())) { if (await Utils.isIpad()) {
sharePositionOrigin = Rect.fromLTWH(0, 0, Get.width, Get.height / 2); 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); RegExp(r'(@(\d+[a-z]_?)*)(\..*)?$', caseSensitive: false);
static String thumbnailImgUrl(String? src, [int? quality]) { static String thumbnailImgUrl(String? src, [int? quality]) {
if (src != null) { if (src != null && quality != 100) {
bool hasMatch = false; bool hasMatch = false;
src = src.splitMapJoin( src = src.splitMapJoin(
regExp, regExp,
@@ -430,7 +430,10 @@ class Utils {
static bool? _isIpad; static bool? _isIpad;
static Future<bool> isIpad() async { static FutureOr<bool> isIpad() async {
if (Platform.isIOS.not) {
return false;
}
if (_isIpad != null) { if (_isIpad != null) {
return _isIpad!; return _isIpad!;
} }
@@ -443,7 +446,7 @@ class Utils {
static void shareText(String text) async { static void shareText(String text) async {
try { try {
Rect? sharePositionOrigin; Rect? sharePositionOrigin;
if (Platform.isIOS && (await isIpad())) { if (await isIpad()) {
sharePositionOrigin = Rect.fromLTWH(0, 0, Get.width, Get.height / 2); sharePositionOrigin = Rect.fromLTWH(0, 0, Get.width, Get.height / 2);
} }
Share.share( 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, static void appSign(Map<String, dynamic> params,
[String appkey = Constants.appKey, String appsec = Constants.appSec]) { [String appkey = Constants.appKey, String appsec = Constants.appSec]) {
params['appkey'] = appkey; params['appkey'] = appkey;

View File

@@ -1440,6 +1440,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.1" 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: protobuf:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -1472,14 +1480,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.2" 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: rxdart:
dependency: "direct overridden" dependency: "direct overridden"
description: description:

View File

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