merge main

This commit is contained in:
guozhigq
2024-09-12 23:56:20 +08:00
39 changed files with 1629 additions and 584 deletions

View File

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:bottom_sheet/bottom_sheet.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/constants.dart';
@ -197,45 +198,38 @@ class VideoIntroController extends GetxController {
SmartDialog.showToast('账号未登录');
return;
}
if (hasCoin.value) {
SmartDialog.showToast('已投过币了');
return;
}
showDialog(
context: Get.context!,
builder: (context) {
return AlertDialog(
title: const Text('选择投币个数'),
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 24),
content: StatefulBuilder(builder: (context, StateSetter setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [1, 2]
.map(
(e) => RadioListTile(
value: e,
title: Text('$e枚'),
groupValue: _tempThemeValue,
onChanged: (value) async {
_tempThemeValue = value!;
setState(() {});
var res = await VideoHttp.coinVideo(
bvid: bvid, multiply: _tempThemeValue);
if (res['status']) {
SmartDialog.showToast('投币成功');
hasCoin.value = true;
videoDetail.value.stat!.coin =
videoDetail.value.stat!.coin! + _tempThemeValue;
} else {
SmartDialog.showToast(res['msg']);
}
Get.back();
},
content: Column(
mainAxisSize: MainAxisSize.min,
children: [1, 2]
.map(
(e) => ListTile(
title: Padding(
padding: const EdgeInsets.only(left: 20),
child: Text('$e '),
),
)
.toList(),
);
}),
onTap: () async {
var res =
await VideoHttp.coinVideo(bvid: bvid, multiply: e);
if (res['status']) {
SmartDialog.showToast('投币成功');
hasCoin.value = true;
videoDetail.value.stat!.coin =
videoDetail.value.stat!.coin! + e;
} else {
SmartDialog.showToast(res['msg']);
}
Get.back();
},
),
)
.toList(),
),
);
});
}

View File

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:appscheme/appscheme.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/gestures.dart';
@ -14,12 +16,14 @@ import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/reply_new/index.dart';
import 'package:pilipala/plugin/pl_gallery/index.dart';
import 'package:pilipala/plugin/pl_popup/index.dart';
import 'package:pilipala/utils/app_scheme.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/url_utils.dart';
import 'package:pilipala/utils/utils.dart';
import 'reply_save.dart';
import 'zan.dart';
Box setting = GStrorage.setting;
@ -32,6 +36,7 @@ class ReplyItem extends StatelessWidget {
this.showReplyRow = true,
this.replyReply,
this.replyType,
this.replySave = false,
super.key,
});
final ReplyItemModel? replyItem;
@ -40,6 +45,7 @@ class ReplyItem extends StatelessWidget {
final bool? showReplyRow;
final Function? replyReply;
final ReplyType? replyType;
final bool? replySave;
@override
Widget build(BuildContext context) {
@ -47,19 +53,28 @@ class ReplyItem extends StatelessWidget {
child: InkWell(
// 点击整个评论区 评论详情/回复
onTap: () {
if (replySave!) {
return;
}
feedBack();
if (replyReply != null) {
replyReply!(replyItem, null, replyItem!.replies!.isNotEmpty);
}
},
onLongPress: () {
if (replySave!) {
return;
}
feedBack();
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return MorePanel(item: replyItem);
return MorePanel(
item: replyItem,
mainFloor: true,
);
},
);
},
@ -232,7 +247,7 @@ class ReplyItem extends StatelessWidget {
),
),
// 操作区域
bottonAction(context, replyItem!.replyControl),
bottonAction(context, replyItem!.replyControl, replySave),
// 一楼的评论
if ((replyItem!.replyControl!.isShow! ||
replyItem!.replies!.isNotEmpty) &&
@ -253,7 +268,7 @@ class ReplyItem extends StatelessWidget {
}
// 感谢、回复、复制
Widget bottonAction(BuildContext context, replyControl) {
Widget bottonAction(BuildContext context, replyControl, replySave) {
ColorScheme colorScheme = Theme.of(context).colorScheme;
TextTheme textTheme = Theme.of(context).textTheme;
return Row(
@ -286,16 +301,26 @@ class ReplyItem extends StatelessWidget {
});
},
child: Row(children: [
Icon(Icons.reply,
size: 18, color: colorScheme.outline.withOpacity(0.8)),
const SizedBox(width: 3),
Text(
'回复',
style: TextStyle(
fontSize: textTheme.labelMedium!.fontSize,
color: colorScheme.outline,
if (!replySave!) ...[
Icon(Icons.reply,
size: 18, color: colorScheme.outline.withOpacity(0.8)),
const SizedBox(width: 3),
Text(
'回复',
style: TextStyle(
fontSize: textTheme.labelMedium!.fontSize,
color: colorScheme.outline,
),
)
],
if (replySave!)
Text(
IdUtils.av2bv(replyItem!.oid!),
style: TextStyle(
fontSize: textTheme.labelMedium!.fontSize,
color: colorScheme.outline,
),
),
),
]),
),
),
@ -436,7 +461,8 @@ class ReplyItemRow extends StatelessWidget {
if (extraRow == 1)
InkWell(
// 一楼点击【共xx条回复】展开评论详情
onTap: () => replyReply!(replyItem),
onTap: () => replyReply?.call(replyItem, null, true),
onLongPress: () => {},
child: Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(8, 5, 8, 8),
@ -549,7 +575,7 @@ InlineSpan buildContent(
);
}
void onPreviewImg(picList, initIndex) {
void onPreviewImg(picList, initIndex, randomInt) {
final MainController mainController = Get.find<MainController>();
mainController.imgPreviewStatus = true;
Navigator.of(context).push(
@ -575,7 +601,7 @@ InlineSpan buildContent(
},
child: Center(
child: Hero(
tag: picList[index],
tag: picList[index] + randomInt,
child: CachedNetworkImage(
fadeInDuration: const Duration(milliseconds: 0),
imageUrl: picList[index],
@ -886,11 +912,12 @@ InlineSpan buildContent(
pictureItem['img_width']))
.truncateToDouble();
} catch (_) {}
String randomInt = Random().nextInt(101).toString();
return Hero(
tag: picList[0],
tag: picList[0] + randomInt,
child: GestureDetector(
onTap: () => onPreviewImg(picList, 0),
onTap: () => onPreviewImg(picList, 0, randomInt),
child: Container(
padding: const EdgeInsets.only(top: 4),
constraints: BoxConstraints(maxHeight: maxHeight),
@ -927,13 +954,14 @@ InlineSpan buildContent(
picList.add(content.pictures[i]['img_src']);
}
for (var i = 0; i < len; i++) {
String randomInt = Random().nextInt(101).toString();
list.add(
LayoutBuilder(
builder: (context, BoxConstraints box) {
return Hero(
tag: picList[i],
tag: picList[i] + randomInt,
child: GestureDetector(
onTap: () => onPreviewImg(picList, i),
onTap: () => onPreviewImg(picList, i, randomInt),
child: NetworkImgLayer(
src: picList[i],
width: box.maxWidth,
@ -1004,7 +1032,12 @@ InlineSpan buildContent(
class MorePanel extends StatelessWidget {
final dynamic item;
const MorePanel({super.key, required this.item});
final bool mainFloor;
const MorePanel({
super.key,
required this.item,
this.mainFloor = false,
});
Future<dynamic> menuActionHandler(String type) async {
String message = item.content.message ?? item.content;
@ -1026,6 +1059,13 @@ class MorePanel extends StatelessWidget {
},
);
break;
case 'save':
Get.back();
Navigator.push(
Get.context!,
PlPopupRoute(child: ReplySave(replyItem: item)),
);
break;
// case 'block':
// SmartDialog.showToast('加入黑名单');
// break;
@ -1076,6 +1116,13 @@ class MorePanel extends StatelessWidget {
leading: const Icon(Icons.copy_outlined, size: 19),
title: Text('自由复制', style: textTheme.titleSmall),
),
if (mainFloor && item.content.pictures.isEmpty)
ListTile(
onTap: () async => await menuActionHandler('save'),
minLeadingWidth: 0,
leading: const Icon(Icons.save_alt_rounded, size: 19),
title: Text('本地保存', style: textTheme.titleSmall),
),
// ListTile(
// onTap: () async => await menuActionHandler('block'),
// minLeadingWidth: 0,

View File

@ -0,0 +1,148 @@
import 'dart:math';
import 'dart:typed_data';
import 'dart:ui';
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:pilipala/models/video/reply/item.dart';
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
import 'package:saver_gallery/saver_gallery.dart';
class ReplySave extends StatefulWidget {
final ReplyItemModel? replyItem;
const ReplySave({required this.replyItem, super.key});
@override
State<ReplySave> createState() => _ReplySaveState();
}
class _ReplySaveState extends State<ReplySave> {
final _boundaryKey = GlobalKey();
void _generatePicWidget() async {
SmartDialog.showLoading(msg: '保存中');
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}";
final result = await SaverGallery.saveImage(
Uint8List.fromList(pngBytes),
name: '$picName.png',
androidRelativePath: "Pictures/PiliPala",
androidExistNotSave: false,
);
if (result.isSuccess) {
SmartDialog.showToast('保存成功');
}
} catch (err) {
print(err);
} finally {
SmartDialog.dismiss();
}
}
List<Widget> _createWidgets(int count, Widget Function() builder) {
return List<Widget>.generate(count, (_) => Expanded(child: builder()));
}
List<Widget> _createColumnWidgets() {
return _createWidgets(3, () => Row(children: _createRowWidgets()));
}
List<Widget> _createRowWidgets() {
return _createWidgets(
4,
() => Center(
child: Transform.rotate(
angle: pi / 10,
child: const Text(
'PiliPala',
style: TextStyle(
color: Color(0x08000000),
fontSize: 18,
fontWeight: FontWeight.bold,
decoration: TextDecoration.none,
),
),
),
),
);
}
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
bottom: false,
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 4.0, sigmaY: 4.0),
child: Container(
width: Get.width,
height: Get.height,
padding: EdgeInsets.fromLTRB(
0,
MediaQuery.of(context).padding.top + 4,
0,
MediaQuery.of(context).padding.bottom + 4,
),
color: Colors.black54,
child: Column(
children: [
Expanded(
child: Container(
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
),
child: Center(
child: SingleChildScrollView(
child: RepaintBoundary(
key: _boundaryKey,
child: IntrinsicHeight(
child: Stack(
children: [
ReplyItem(
replyItem: widget.replyItem,
showReplyRow: false,
replySave: true,
),
Positioned.fill(
child: Column(
children: _createColumnWidgets(),
),
),
],
),
),
),
),
),
),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FilledButton(
onPressed: () => Get.back(),
child: const Text('取消'),
),
const SizedBox(width: 40),
FilledButton(
onPressed: _generatePicWidget,
child: const Text('保存'),
),
],
),
],
),
),
),
);
}
}

View File

@ -21,7 +21,7 @@ class VideoReplyReplyPanel extends StatefulWidget {
this.replyType,
this.sheetHeight,
this.currentReply,
this.loadMore,
this.loadMore = true,
super.key,
});
final int? oid;
@ -32,7 +32,7 @@ class VideoReplyReplyPanel extends StatefulWidget {
final ReplyType? replyType;
final double? sheetHeight;
final dynamic currentReply;
final bool? loadMore;
final bool loadMore;
@override
State<VideoReplyReplyPanel> createState() => _VideoReplyReplyPanelState();
@ -142,7 +142,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
),
),
],
widget.loadMore != null && widget.loadMore!
widget.loadMore
? FutureBuilder(
future: _futureBuilderFuture,
builder: (BuildContext context, snapshot) {

View File

@ -63,7 +63,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
late bool autoPlayEnable;
late bool autoPiP;
late Floating floating;
bool isShowing = true;
RxBool isShowing = true.obs;
// 生命周期监听
late final AppLifecycleListener _lifecycleListener;
late double statusHeight;
@ -183,6 +183,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController!.addStatusLister(playerListener);
vdCtr.autoPlay.value = true;
vdCtr.isShowCover.value = false;
isShowing.value = true;
autoEnterPip(status: PlayerStatus.playing);
}
@ -258,7 +259,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController!.pause();
vdCtr.clearSubtitleContent();
}
setState(() => isShowing = false);
isShowing.value = false;
super.didPushNext();
}
@ -272,10 +273,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
if (plPlayerController != null &&
plPlayerController!.videoPlayerController != null) {
setState(() {
vdCtr.setSubtitleContent();
isShowing = true;
});
vdCtr.setSubtitleContent();
isShowing.value = true;
}
vdCtr.isFirstTime = false;
final bool autoplay = autoPlayEnable;
@ -330,12 +329,14 @@ class _VideoDetailPageState extends State<VideoDetailPage>
plPlayerController?.danmakuController?.clear();
break;
case 'pause':
vdCtr.hiddenReplyReplyPanel();
if (vdCtr.videoType == SearchType.video) {
videoIntroController.hiddenEpisodeBottomSheet();
}
if (vdCtr.videoType == SearchType.media_bangumi) {
bangumiIntroController.hiddenEpisodeBottomSheet();
if (autoPiP) {
vdCtr.hiddenReplyReplyPanel();
if (vdCtr.videoType == SearchType.video) {
videoIntroController.hiddenEpisodeBottomSheet();
}
if (vdCtr.videoType == SearchType.media_bangumi) {
bangumiIntroController.hiddenEpisodeBottomSheet();
}
}
break;
}
@ -650,7 +651,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
tag: heroTag,
child: Stack(
children: <Widget>[
if (isShowing) buildVideoPlayerPanel(),
Obx(
() => isShowing.value
? buildVideoPlayerPanel()
: const SizedBox(),
),
/// 关闭自动播放时 手动播放
Obx(