Merge branch 'feature-m3Design'

This commit is contained in:
guozhigq
2023-07-26 11:44:12 +08:00
84 changed files with 1288 additions and 855 deletions

View File

@ -1,6 +1,4 @@
import 'dart:async';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_meedu_media_kit/meedu_player.dart';
import 'package:get/get.dart';

View File

@ -105,7 +105,6 @@ class VideoIntroController extends GetxController {
// 获取up主粉丝数
Future queryUserStat() async {
var result = await UserHttp.userStat(mid: videoDetail.value.owner!.mid!);
print('🌹:$result');
if (result['status']) {
userStat = result['data'];
}
@ -185,9 +184,11 @@ class VideoIntroController extends GetxController {
if (!hasLike.value) {
SmartDialog.showToast('点赞成功 👍');
hasLike.value = true;
videoDetail.value.stat!.like = videoDetail.value.stat!.like! + 1;
} else if (hasLike.value) {
SmartDialog.showToast('取消赞');
hasLike.value = false;
videoDetail.value.stat!.like = videoDetail.value.stat!.like! - 1;
}
hasLike.refresh();
} else {
@ -238,14 +239,15 @@ class VideoIntroController extends GetxController {
onPressed: () async {
var res = await VideoHttp.coinVideo(
bvid: bvid, multiply: _tempThemeValue);
print(res);
if (res['status']) {
SmartDialog.showToast('投币成功');
SmartDialog.showToast('投币成功 👏');
hasCoin.value = true;
videoDetail.value.stat!.coin =
videoDetail.value.stat!.coin! + _tempThemeValue;
} else {
SmartDialog.showToast(res['msg']);
}
Get.back();
queryHasCoinVideo();
},
child: const Text('确定'))
],
@ -263,7 +265,10 @@ class VideoIntroController extends GetxController {
delMediaIdsNew.add(i.id);
}
}
} catch (e) {}
} catch (e) {
// ignore: avoid_print
print(e);
}
var result = await VideoHttp.favVideo(
aid: IdUtils.bv2av(bvid),
addIds: addMediaIdsNew.join(','),
@ -282,10 +287,8 @@ class VideoIntroController extends GetxController {
// 分享视频
Future actionShareVideo() async {
var result =
await Share.share('${HttpString.baseUrl}/video/$bvid').whenComplete(() {
print("share completion block ");
});
var result = await Share.share('${HttpString.baseUrl}/video/$bvid')
.whenComplete(() {});
return result;
}

View File

@ -1,4 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
@ -7,10 +6,7 @@ import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/fav/index.dart';
import 'package:pilipala/pages/favDetail/index.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/widgets/expandable_section.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart';
@ -19,7 +15,9 @@ import 'package:pilipala/pages/video/detail/introduction/controller.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
import 'widgets/action_row_item.dart';
import 'widgets/fav_panel.dart';
import 'widgets/intro_detail.dart';
import 'widgets/season.dart';
class VideoIntroPanel extends StatefulWidget {
@ -55,6 +53,7 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
@override
Widget build(BuildContext context) {
super.build(context);
return FutureBuilder(
future: videoIntroController.queryVideoIntro(),
builder: (context, snapshot) {
@ -79,10 +78,10 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
}
class VideoInfo extends StatefulWidget {
bool loadingStatus = false;
VideoDetailData? videoDetail;
final bool loadingStatus;
final VideoDetailData? videoDetail;
VideoInfo({Key? key, required this.loadingStatus, this.videoDetail})
const VideoInfo({Key? key, this.loadingStatus = false, this.videoDetail})
: super(key: key);
@override
@ -95,14 +94,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Get.put(VideoIntroController(), tag: Get.arguments['heroTag']);
bool isExpand = false;
/// 手动控制动画的控制器
late AnimationController? _manualController;
/// 手动控制
late Animation<double>? _manualAnimation;
final FavController _favController = Get.put(FavController());
late VideoDetailController? videoDetailCtr;
Box localCache = GStrorage.localCache;
late double sheetHeight;
@ -111,13 +102,6 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
void initState() {
super.initState();
/// 不设置重复使用代码控制进度动画时间1秒
_manualController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
);
_manualAnimation =
Tween<double>(begin: 0.5, end: 1.5).animate(_manualController!);
videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
sheetHeight = localCache.get('sheetHeight');
@ -141,143 +125,123 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return SliverPadding(
padding: const EdgeInsets.only(left: 12, right: 12, top: 10),
padding: const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace, top: 13),
sliver: SliverToBoxAdapter(
child: !widget.loadingStatus || videoItem.isNotEmpty
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SelectableRegion(
magnifierConfiguration: const TextMagnifierConfiguration(),
focusNode: FocusNode(),
selectionControls: MaterialTextSelectionControls(),
child: Text(
!widget.loadingStatus
? widget.videoDetail!.title
: videoItem['title'],
style: Theme.of(context).textTheme.titleMedium!.copyWith(
letterSpacing: 0.5,
),
),
),
InkWell(
splashColor: Colors.transparent,
hoverColor: Colors.transparent,
highlightColor: Colors.transparent,
GestureDetector(
onTap: () {
_manualController!.animateTo(isExpand ? 0 : 0.5);
setState(() {
isExpand = !isExpand;
});
showBottomSheet(
context: context,
enableDrag: true,
builder: (BuildContext context) {
return IntroDetail(videoDetail: widget.videoDetail!);
},
);
},
child: Row(
children: [
const SizedBox(width: 2),
StatView(
theme: 'gray',
view: !widget.loadingStatus
? widget.videoDetail!.stat!.view
: videoItem['stat'].view,
size: 'medium',
),
const SizedBox(width: 10),
StatDanMu(
theme: 'gray',
danmu: !widget.loadingStatus
? widget.videoDetail!.stat!.danmaku
: videoItem['stat'].danmaku,
size: 'medium',
),
const SizedBox(width: 10),
Text(
Utils.dateFormat(
!widget.loadingStatus
? widget.videoDetail!.pubdate
: videoItem['pubdate'],
formatType: 'detail'),
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline),
),
const Spacer(),
RotationTransition(
turns: _manualAnimation!,
child: SizedBox(
width: 35,
height: 35,
child: IconButton(
padding: const EdgeInsets.all(2.0),
onPressed: () {
/// 0.5代表 180弧度
_manualController!
.animateTo(isExpand ? 0 : 0.5);
setState(() {
isExpand = !isExpand;
});
},
icon: Icon(
FontAwesomeIcons.angleUp,
size: 15,
color: Theme.of(context).colorScheme.outline,
),
Expanded(
child: Text(
!widget.loadingStatus
? widget.videoDetail!.title
: videoItem['title'],
style: const TextStyle(
fontSize: 18,
letterSpacing: 0.3,
fontWeight: FontWeight.w500,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 20),
SizedBox(
width: 34,
height: 34,
child: IconButton(
style: ButtonStyle(
padding:
MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor:
MaterialStateProperty.resolveWith((states) {
return Theme.of(context)
.highlightColor
.withOpacity(0.2);
}),
),
onPressed: () {
showBottomSheet(
context: context,
enableDrag: true,
builder: (BuildContext context) {
return IntroDetail(
videoDetail: widget.videoDetail!);
},
);
},
icon: const Icon(Icons.more_horiz),
),
),
const SizedBox(width: 10),
],
),
),
// 简介 默认收起
if (!widget.loadingStatus)
ExpandedSection(
expand: isExpand,
begin: 0.0,
end: 1.0,
child: DefaultTextStyle(
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
height: 1.5,
fontSize:
Theme.of(context).textTheme.labelMedium?.fontSize,
),
child: Padding(
padding: const EdgeInsets.only(bottom: 10),
child: SelectableRegion(
magnifierConfiguration:
const TextMagnifierConfiguration(),
focusNode: FocusNode(),
selectionControls: MaterialTextSelectionControls(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(widget.videoDetail!.bvid!),
Text.rich(
TextSpan(
children: [
buildContent(
context, widget.videoDetail!),
],
),
),
],
),
),
),
const SizedBox(height: 6),
Row(
children: [
const SizedBox(width: 2),
StatView(
theme: 'black',
view: !widget.loadingStatus
? widget.videoDetail!.stat!.view
: videoItem['stat'].view,
size: 'medium',
),
const SizedBox(width: 10),
StatDanMu(
theme: 'black',
danmu: !widget.loadingStatus
? widget.videoDetail!.stat!.danmaku
: videoItem['stat'].danmaku,
size: 'medium',
),
const SizedBox(width: 10),
Text(
Utils.dateFormat(
!widget.loadingStatus
? widget.videoDetail!.pubdate
: videoItem['pubdate'],
formatType: 'detail'),
style: const TextStyle(fontSize: 12),
),
],
),
// 点赞收藏转发
Padding(
padding: const EdgeInsets.only(top: 15),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: actionRow(
context,
videoIntroController,
videoDetailCtr,
),
),
const SizedBox(height: 8),
// 点赞收藏转发
_actionGrid(context, videoIntroController, videoDetailCtr),
),
// 合集
if (!widget.loadingStatus &&
widget.videoDetail!.ugcSeason != null) ...[
seasonPanel(widget.videoDetail!.ugcSeason!,
widget.videoDetail!.pages!.first.cid, sheetHeight)
],
Divider(
height: 26,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
// Divider(
// height: 26,
// color: Theme.of(context).dividerColor.withOpacity(0.1),
// ),
const SizedBox(height: 20),
GestureDetector(
onTap: () {
int mid = !widget.loadingStatus
@ -293,53 +257,84 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
},
child: Row(
children: [
const SizedBox(width: 5),
NetworkImgLayer(
type: 'avatar',
src: !widget.loadingStatus
? widget.videoDetail!.owner!.face
: videoItem['owner'].face,
width: 38,
height: 38,
width: 34,
height: 34,
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
),
const SizedBox(width: 14),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(!widget.loadingStatus
? widget.videoDetail!.owner!.name
: videoItem['owner'].name),
// const SizedBox(width: 10),
Text(
widget.loadingStatus
? '- 粉丝'
: '${Utils.numFormat(videoIntroController.userStat['follower'])}粉丝',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize,
color: Theme.of(context).colorScheme.outline),
),
],
const SizedBox(width: 10),
Text(
!widget.loadingStatus
? widget.videoDetail!.owner!.name
: videoItem['owner'].name,
style: const TextStyle(fontSize: 13),
),
const SizedBox(width: 10),
Text(
widget.loadingStatus
? '- 粉丝'
: Utils.numFormat(
videoIntroController.userStat['follower']),
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize,
color: Theme.of(context).colorScheme.outline),
),
const Spacer(),
AnimatedOpacity(
opacity: widget.loadingStatus ? 0 : 1,
duration: const Duration(milliseconds: 150),
child: SizedBox(
height: 36,
height: 32,
child: Obx(
() => videoIntroController.followStatus.isNotEmpty
? ElevatedButton(
? TextButton(
onPressed: () => videoIntroController
.actionRelationMod(),
child: Text(videoIntroController
.followStatus['attribute'] ==
0
? '关注'
: '已关注'),
style: TextButton.styleFrom(
padding: const EdgeInsets.only(
left: 8, right: 8),
foregroundColor:
videoIntroController.followStatus[
'attribute'] !=
0
? Theme.of(context)
.colorScheme
.outline
: Theme.of(context)
.colorScheme
.onPrimary,
backgroundColor:
videoIntroController.followStatus[
'attribute'] !=
0
? Theme.of(context)
.colorScheme
.onInverseSurface
: Theme.of(context)
.colorScheme
.primary, // 设置按钮背景色
),
child: Text(
videoIntroController.followStatus[
'attribute'] !=
0
? '已关注'
: '关注',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
),
)
: ElevatedButton(
onPressed: () => videoIntroController
@ -349,110 +344,87 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
),
),
),
const SizedBox(width: 4)
],
),
),
Divider(
height: 26,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
// const SizedBox(height: 10),
const SizedBox(height: 12),
// Divider(
// height: 12,
// color: Theme.of(context).dividerColor.withOpacity(0.1),
// ),
],
)
: const Center(child: CircularProgressIndicator()),
: const SizedBox(
height: 100,
child: Center(
child: CircularProgressIndicator(),
),
),
),
);
}
// 喜欢 投币 分享
Widget _actionGrid(
BuildContext context, videoIntroController, videoDetailCtr) {
return LayoutBuilder(builder: (context, constraints) {
return SizedBox(
height: constraints.maxWidth / 5 * 0.8,
child: Material(
child: GridView.count(
primary: false,
padding: const EdgeInsets.all(0),
crossAxisCount: 5,
childAspectRatio: 1.25,
children: <Widget>[
// InkWell(
// onTap: () => videoIntroController.actionOneThree(),
// borderRadius: StyleString.mdRadius,
// child: Padding(
// padding: const EdgeInsets.all(12),
// child: Image.asset(
// 'assets/images/logo/logo_big.png',
// width: 10,
// height: 10,
// ),
// ),
// ),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: () => videoIntroController.actionLikeVideo(),
selectStatus: videoIntroController.hasLike.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.like!.toString()
: '-'),
),
// ActionItem(
// icon: const Icon(FontAwesomeIcons.thumbsDown),
// selectIcon: const Icon(FontAwesomeIcons.solidThumbsDown),
// onTap: () => {},
// selectStatus: false,
// loadingStatus: widget.loadingStatus,
// text: '不喜欢'),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: () => videoIntroController.actionCoinVideo(),
selectStatus: videoIntroController.hasCoin.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.coin!.toString()
: '-'),
),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.heart),
selectIcon: const Icon(FontAwesomeIcons.heartCircleCheck),
onTap: () => showFavBottomSheet(),
selectStatus: videoIntroController.hasFav.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.favorite!.toString()
: '-'),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare),
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.share!.toString()
: '-'),
ActionItem(
icon: const Icon(FontAwesomeIcons.comments),
onTap: () {
videoDetailCtr.tabCtr.animateTo(1);
},
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.reply!.toString()
: '-'),
],
),
Widget actionRow(BuildContext context, videoIntroController, videoDetailCtr) {
return Row(children: [
Obx(
() => ActionRowItem(
icon: const Icon(FontAwesomeIcons.thumbsUp),
onTap: () => videoIntroController.actionLikeVideo(),
selectStatus: videoIntroController.hasLike.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.like!.toString()
: '-',
),
);
});
),
const SizedBox(width: 8),
Obx(
() => ActionRowItem(
icon: const Icon(FontAwesomeIcons.b),
onTap: () => videoIntroController.actionCoinVideo(),
selectStatus: videoIntroController.hasCoin.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.coin!.toString()
: '-',
),
),
const SizedBox(width: 8),
Obx(
() => ActionRowItem(
icon: const Icon(FontAwesomeIcons.heart),
onTap: () => showFavBottomSheet(),
selectStatus: videoIntroController.hasFav.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.favorite!.toString()
: '-',
),
),
const SizedBox(width: 8),
ActionRowItem(
icon: const Icon(FontAwesomeIcons.comment),
onTap: () {
videoDetailCtr.tabCtr.animateTo(1);
},
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.reply!.toString()
: '-',
),
const SizedBox(width: 8),
ActionRowItem(
icon: const Icon(FontAwesomeIcons.share),
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: widget.loadingStatus,
// text: !widget.loadingStatus
// ? widget.videoDetail!.stat!.share!.toString()
// : '-',
text: '转发'),
]);
}
InlineSpan buildContent(BuildContext context, content) {
@ -491,54 +463,3 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
return TextSpan(children: spanChilds);
}
}
class ActionItem extends StatelessWidget {
Icon? icon;
Icon? selectIcon;
Function? onTap;
bool? loadingStatus;
String? text;
bool selectStatus = false;
ActionItem({
Key? key,
this.icon,
this.selectIcon,
this.onTap,
this.loadingStatus,
this.text,
required this.selectStatus,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => onTap!(),
borderRadius: StyleString.mdRadius,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 4),
selectStatus
? Icon(selectIcon!.icon!,
size: 21, color: Theme.of(context).primaryColor)
: Icon(icon!.icon!,
size: 21, color: Theme.of(context).colorScheme.outline),
const SizedBox(height: 4),
AnimatedOpacity(
opacity: loadingStatus! ? 0 : 1,
duration: const Duration(milliseconds: 200),
child: Text(
text ?? '',
style: TextStyle(
color: selectStatus
? Theme.of(context).primaryColor
: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context).textTheme.labelSmall?.fontSize),
),
),
],
),
);
}
}

View File

@ -0,0 +1,53 @@
import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart';
class ActionItem extends StatelessWidget {
final Icon? icon;
final Icon? selectIcon;
final Function? onTap;
final bool? loadingStatus;
final String? text;
final bool selectStatus;
const ActionItem({
Key? key,
this.icon,
this.selectIcon,
this.onTap,
this.loadingStatus,
this.text,
this.selectStatus = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return InkWell(
onTap: () => onTap!(),
borderRadius: StyleString.mdRadius,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const SizedBox(height: 4),
selectStatus
? Icon(selectIcon!.icon!,
size: 21, color: Theme.of(context).primaryColor)
: Icon(icon!.icon!,
size: 21, color: Theme.of(context).colorScheme.outline),
const SizedBox(height: 4),
AnimatedOpacity(
opacity: loadingStatus! ? 0 : 1,
duration: const Duration(milliseconds: 200),
child: Text(
text ?? '',
style: TextStyle(
color: selectStatus
? Theme.of(context).primaryColor
: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context).textTheme.labelSmall?.fontSize),
),
),
],
),
);
}
}

View File

@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
class ActionRowItem extends StatelessWidget {
final Icon? icon;
final Icon? selectIcon;
final Function? onTap;
final bool? loadingStatus;
final String? text;
final bool selectStatus;
const ActionRowItem({
Key? key,
this.icon,
this.selectIcon,
this.onTap,
this.loadingStatus,
this.text,
this.selectStatus = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Material(
color: selectStatus
? Theme.of(context).colorScheme.primaryContainer.withOpacity(0.6)
: Theme.of(context).highlightColor.withOpacity(0.2),
borderRadius: const BorderRadius.all(Radius.circular(30)),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => onTap!(),
child: Padding(
padding: const EdgeInsets.fromLTRB(13, 6.5, 15, 6.3),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (icon != null) ...[
Icon(icon!.icon!,
size: 13,
color: selectStatus
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSecondaryContainer),
const SizedBox(width: 6),
],
AnimatedOpacity(
opacity: loadingStatus! ? 0 : 1,
duration: const Duration(milliseconds: 200),
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder:
(Widget child, Animation<double> animation) {
return ScaleTransition(scale: animation, child: child);
},
child: Text(
text ?? '',
key: ValueKey<String>(text ?? ''),
style: TextStyle(
color: selectStatus
? Theme.of(context).colorScheme.primary
: null,
fontSize:
Theme.of(context).textTheme.labelSmall!.fontSize),
),
),
),
],
),
),
),
);
}
}

View File

@ -5,8 +5,8 @@ import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/utils/storage.dart';
class FavPanel extends StatefulWidget {
var ctr;
FavPanel({this.ctr});
final dynamic ctr;
const FavPanel({super.key, this.ctr});
@override
State<FavPanel> createState() => _FavPanelState();
@ -111,7 +111,7 @@ class _FavPanelState extends State<FavPanel> {
}
} else {
// 骨架屏
return Text('请求中');
return const Text('请求中');
}
},
),

View File

@ -0,0 +1,149 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
Box localCache = GStrorage.localCache;
late double sheetHeight;
class IntroDetail extends StatelessWidget {
final dynamic videoDetail;
const IntroDetail({
Key? key,
this.videoDetail,
}) : super(key: key);
@override
Widget build(BuildContext context) {
sheetHeight = localCache.get('sheetHeight');
return Container(
color: Theme.of(context).colorScheme.background,
padding: const EdgeInsets.only(left: 14, right: 14),
height: sheetHeight,
child: Column(
children: [
Container(
height: 35,
padding: const EdgeInsets.only(bottom: 2),
child: Center(
child: Container(
width: 32,
height: 3,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.onSecondaryContainer
.withOpacity(0.5),
borderRadius: const BorderRadius.all(Radius.circular(3))),
),
),
),
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
videoDetail!.title,
style: const TextStyle(
fontSize: 18,
letterSpacing: 0.5,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 10),
Row(
children: [
const SizedBox(width: 2),
StatView(
theme: 'black',
view: videoDetail!.stat!.view,
size: 'medium',
),
const SizedBox(width: 10),
StatDanMu(
theme: 'black',
danmu: videoDetail!.stat!.danmaku,
size: 'medium',
),
const SizedBox(width: 10),
Text(
Utils.dateFormat(videoDetail!.pubdate,
formatType: 'detail'),
style: const TextStyle(fontSize: 12),
),
],
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: SelectableRegion(
magnifierConfiguration:
const TextMagnifierConfiguration(),
focusNode: FocusNode(),
selectionControls: MaterialTextSelectionControls(),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(videoDetail!.bvid!),
const SizedBox(height: 4),
Text.rich(
TextSpan(
children: [
buildContent(context, videoDetail!),
],
),
),
],
),
),
),
],
),
),
)
],
));
}
InlineSpan buildContent(BuildContext context, content) {
String desc = content.desc;
List descV2 = content.descV2;
// type
// 1 普通文本
// 2 @用户
List<InlineSpan> spanChilds = [];
if (descV2.isNotEmpty) {
for (var i = 0; i < descV2.length; i++) {
if (descV2[i].type == 1) {
spanChilds.add(TextSpan(text: descV2[i].rawText));
} else if (descV2[i].type == 2) {
spanChilds.add(
TextSpan(
text: '@${descV2[i].rawText}',
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () {
String heroTag = Utils.makeHeroTag(descV2[i].bizId);
Get.toNamed(
'/member?mid=${descV2[i].bizId}',
arguments: {'face': '', 'heroTag': heroTag},
);
},
),
);
}
}
} else {
spanChilds.add(TextSpan(text: desc));
}
return TextSpan(children: spanChilds);
}
}

View File

@ -0,0 +1,93 @@
import 'package:flutter/material.dart';
class MenuRow extends StatelessWidget {
final bool? loadingStatus;
const MenuRow({
Key? key,
this.loadingStatus,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
color: Theme.of(context).colorScheme.background,
padding: const EdgeInsets.only(top: 9, bottom: 9, left: 12),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(children: [
actionRowLineItem(
context,
() => {},
loadingStatus,
'推荐',
selectStatus: true,
),
const SizedBox(width: 8),
actionRowLineItem(
context,
() => {},
loadingStatus,
'弹幕',
selectStatus: false,
),
const SizedBox(width: 8),
actionRowLineItem(
context,
() => {},
loadingStatus,
'评论列表',
selectStatus: false,
),
const SizedBox(width: 8),
actionRowLineItem(
context,
() => {},
loadingStatus,
'播放列表',
selectStatus: false,
),
]),
),
);
}
Widget actionRowLineItem(
context, Function? onTap, bool? loadingStatus, String? text,
{bool selectStatus = false}) {
return Material(
color: selectStatus
? Theme.of(context).highlightColor.withOpacity(0.2)
: Colors.transparent,
borderRadius: const BorderRadius.all(Radius.circular(30)),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => onTap!(),
child: Container(
padding: const EdgeInsets.fromLTRB(13, 5.5, 13, 5.5),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(30)),
border: Border.all(
color: selectStatus
? Colors.transparent
: Theme.of(context).highlightColor.withOpacity(0.2),
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
AnimatedOpacity(
opacity: loadingStatus! ? 0 : 1,
duration: const Duration(milliseconds: 200),
child: Text(
text!,
style: const TextStyle(fontSize: 13),
),
),
],
),
),
),
);
}
}

View File

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/models/video_detail_res.dart';
Widget seasonPanel(UgcSeason ugcSeason, cid, sheetHeight) {
@ -75,21 +74,28 @@ Widget seasonPanel(UgcSeason ugcSeason, cid, sheetHeight) {
),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(10, 10, 10, 10),
padding: const EdgeInsets.fromLTRB(8, 12, 8, 12),
child: Row(
children: [
Expanded(
child: Text(
'合集:${ugcSeason.title!}',
style: Theme.of(context).textTheme.labelMedium,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 15),
Image.asset(
'assets/images/live.gif',
color: Theme.of(context).colorScheme.primary,
height: 11,
),
const SizedBox(width: 4),
Text(
'${currentIndex + 1} / ${ugcSeason.epCount}',
style: Theme.of(context).textTheme.labelSmall,
style: Theme.of(context).textTheme.labelMedium,
),
const SizedBox(width: 2),
const SizedBox(width: 6),
const Icon(
Icons.arrow_forward_ios_outlined,
size: 13,

View File

@ -114,6 +114,7 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
@override
Widget build(BuildContext context) {
super.build(context);
return RefreshIndicator(
onRefresh: () async {
_videoReplyController.currentPage = 0;
@ -130,8 +131,18 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
floating: true,
delegate: _MySliverPersistentHeaderDelegate(
child: Container(
color: Theme.of(context).colorScheme.background,
padding: const EdgeInsets.fromLTRB(12, 6, 10, 6),
height: 45,
padding: const EdgeInsets.fromLTRB(12, 0, 6, 0),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
border: Border(
bottom: BorderSide(
color: Theme.of(context)
.colorScheme
.outline
.withOpacity(0.1)),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -155,9 +166,11 @@ class _VideoReplyPanelState extends State<VideoReplyPanel>
child: TextButton.icon(
onPressed: () =>
_videoReplyController.queryBySort(),
icon: const Icon(Icons.sort, size: 17),
icon: const Icon(Icons.sort, size: 15),
label: Obx(() => Text(
_videoReplyController.sortTypeLabel.value)),
_videoReplyController.sortTypeLabel.value,
style: const TextStyle(fontSize: 12),
)),
),
)
],

View File

@ -12,21 +12,21 @@ import 'package:pilipala/utils/utils.dart';
import 'zan.dart';
class ReplyItem extends StatelessWidget {
ReplyItem({
super.key,
const ReplyItem({
this.replyItem,
this.addReply,
this.replyLevel,
this.showReplyRow,
this.showReplyRow = true,
this.replyReply,
this.replyType,
});
ReplyItemModel? replyItem;
Function? addReply;
String? replyLevel;
bool? showReplyRow = true;
Function? replyReply;
ReplyType? replyType;
Key? key,
}) : super(key: key);
final ReplyItemModel? replyItem;
final Function? addReply;
final String? replyLevel;
final bool? showReplyRow;
final Function? replyReply;
final ReplyType? replyType;
@override
Widget build(BuildContext context) {
@ -108,8 +108,7 @@ class ReplyItem extends StatelessWidget {
replyItem!.member!.vip!['vipStatus'] > 0
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
fontSize:
Theme.of(context).textTheme.titleSmall!.fontSize,
fontSize: 13,
),
),
const SizedBox(width: 6),
@ -118,7 +117,7 @@ class ReplyItem extends StatelessWidget {
height: 11,
),
const SizedBox(width: 6),
if (replyItem!.isUp!) UpTag(),
if (replyItem!.isUp!) const UpTag(),
],
),
Positioned(
@ -186,7 +185,7 @@ class ReplyItem extends StatelessWidget {
TextSpan(
children: [
if (replyItem!.isTop!)
WidgetSpan(child: UpTag(tagText: 'TOP')),
const WidgetSpan(child: UpTag(tagText: 'TOP')),
buildContent(context, replyItem!, replyReply, null),
],
),
@ -204,7 +203,7 @@ class ReplyItem extends StatelessWidget {
child: ReplyItemRow(
replies: replyItem!.replies,
replyControl: replyItem!.replyControl,
f_rpid: replyItem!.rpid,
// f_rpid: replyItem!.rpid,
replyItem: replyItem,
replyReply: replyReply,
),
@ -216,7 +215,6 @@ class ReplyItem extends StatelessWidget {
// 感谢、回复、复制
Widget bottonAction(context, replyControl) {
var color = Theme.of(context).colorScheme.outline;
return Row(
children: [
const SizedBox(width: 48),
@ -297,13 +295,13 @@ class ReplyItemRow extends StatelessWidget {
super.key,
this.replies,
this.replyControl,
this.f_rpid,
// this.f_rpid,
this.replyItem,
this.replyReply,
});
List? replies;
ReplyControl? replyControl;
int? f_rpid;
// int? f_rpid;
ReplyItemModel? replyItem;
Function? replyReply;
@ -361,7 +359,7 @@ class ReplyItemRow extends StatelessWidget {
},
),
if (replies![i].isUp)
WidgetSpan(
const WidgetSpan(
child: UpTag(),
),
buildContent(
@ -439,16 +437,14 @@ InlineSpan buildContent(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () => {
Get.toNamed(
'/webview',
parameters: {
'url': content.vote['url'],
'type': 'vote',
'pageTitle': content.vote['title'],
},
)
},
..onTap = () => Get.toNamed(
'/webview',
parameters: {
'url': content.vote['url'],
'type': 'vote',
'pageTitle': content.vote['title'],
},
),
),
);
return '';
@ -554,11 +550,9 @@ InlineSpan buildContent(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () => {
Get.toNamed('/searchResult', parameters: {
'keyword': content.jumpUrl[matchStr]['title']
})
},
..onTap = () => Get.toNamed('/searchResult', parameters: {
'keyword': content.jumpUrl[matchStr]['title']
}),
),
);
spanChilds.add(
@ -718,16 +712,14 @@ InlineSpan buildContent(
color: Theme.of(context).colorScheme.primary,
),
recognizer: TapGestureRecognizer()
..onTap = () => {
Get.toNamed(
'/webview',
parameters: {
'url': content.richText['note']['click_url'],
'type': 'note',
'pageTitle': '笔记预览'
},
)
},
..onTap = () => Get.toNamed(
'/webview',
parameters: {
'url': content.richText['note']['click_url'],
'type': 'note',
'pageTitle': '笔记预览'
},
),
),
);
}
@ -736,8 +728,8 @@ InlineSpan buildContent(
}
class UpTag extends StatelessWidget {
String? tagText;
UpTag({super.key, this.tagText = 'UP'});
final String? tagText;
const UpTag({super.key, this.tagText = 'UP'});
@override
Widget build(BuildContext context) {
Color primary = Theme.of(context).colorScheme.primary;

View File

@ -6,13 +6,13 @@ import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/models/video/reply/item.dart';
class ZanButton extends StatefulWidget {
ZanButton({
const ZanButton({
super.key,
this.replyItem,
this.replyType,
});
ReplyItemModel? replyItem;
final ReplyItemModel? replyItem;
final ReplyType? replyType;
@override

View File

@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
@ -10,13 +9,14 @@ import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/utils/storage.dart';
class VideoReplyNewDialog extends StatefulWidget {
int? oid;
int? root;
int? parent;
ReplyType? replyType;
ReplyItemModel? replyItem;
final int? oid;
final int? root;
final int? parent;
final ReplyType? replyType;
final ReplyItemModel? replyItem;
VideoReplyNewDialog({
const VideoReplyNewDialog({
super.key,
this.oid,
this.root,
this.parent,
@ -56,7 +56,9 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
_autoFocus() async {
await Future.delayed(const Duration(milliseconds: 300));
FocusScope.of(context).requestFocus(replyContentFocusNode);
if (context.mounted) {
FocusScope.of(context).requestFocus(replyContentFocusNode);
}
}
_printLatestValue() {
@ -91,9 +93,8 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
super.didChangeMetrics();
WidgetsBinding.instance.addPostFrameCallback((_) {
// 键盘高度
final viewInsets = EdgeInsets.fromWindowPadding(
WidgetsBinding.instance.window.viewInsets,
WidgetsBinding.instance.window.devicePixelRatio);
final viewInsets = EdgeInsets.fromViewPadding(
View.of(context).viewInsets, View.of(context).devicePixelRatio);
_debouncer.run(() {
if (mounted) {
setState(() {
@ -197,7 +198,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
}
}
typedef void DebounceCallback();
typedef DebounceCallback = void Function();
class Debouncer {
DebounceCallback? callback;

View File

@ -81,7 +81,7 @@ class _VideoReplyReplyPanelState extends State<VideoReplyReplyPanel> {
if (widget.source == 'videoDetail')
Container(
height: 45,
padding: const EdgeInsets.only(left: 14, right: 14),
padding: const EdgeInsets.only(left: 12, right: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [

View File

@ -5,6 +5,8 @@ import 'package:flutter_meedu_media_kit/meedu_player.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/sliver_header.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart';
import 'package:pilipala/pages/video/detail/controller.dart';
import 'package:pilipala/pages/video/detail/introduction/index.dart';
@ -171,8 +173,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
controller: _meeduPlayerController!,
header: (BuildContext context,
MeeduPlayerController
_meeduPlayerController,
Responsive) {
meeduPlayerController,
Responsive responsive) {
return AppBar(
toolbarHeight: 40,
backgroundColor: Colors.transparent,
@ -230,7 +232,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
children: [
Container(
width: double.infinity,
height: 45,
height: 0,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
@ -250,8 +252,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
() => TabBar(
controller: videoDetailController.tabCtr,
dividerColor: Colors.transparent,
// indicatorColor:
// Theme.of(context).colorScheme.background,
indicatorColor:
Theme.of(context).colorScheme.background,
tabs: videoDetailController.tabs
.map((String name) => Tab(text: name))
.toList(),
@ -267,11 +269,20 @@ class _VideoDetailPageState extends State<VideoDetailPage>
children: [
Builder(
builder: (context) {
return const CustomScrollView(
key: PageStorageKey<String>('简介'),
return CustomScrollView(
key: const PageStorageKey<String>('简介'),
slivers: <Widget>[
VideoIntroPanel(),
RelatedVideoPanel(),
const VideoIntroPanel(),
SliverPersistentHeader(
floating: true,
pinned: true,
delegate: SliverHeaderDelegate(
height: 50,
child:
const MenuRow(loadingStatus: false),
),
),
const RelatedVideoPanel(),
],
);
},
@ -296,6 +307,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
snapshot.data!.toDouble(),
continuePlay,
playerStatus,
null,
);
}),
)

View File

@ -3,14 +3,15 @@ import 'package:flutter_meedu_media_kit/meedu_player.dart';
class ScrollAppBar extends StatelessWidget {
final double scrollVal;
Function callback;
final Function callback;
final PlayerStatus playerStatus;
ScrollAppBar(
const ScrollAppBar(
this.scrollVal,
this.callback,
this.playerStatus,
);
Key? key,
) : super(key: key);
@override
Widget build(BuildContext context) {

View File

@ -1,16 +1,20 @@
// ignore_for_file: library_private_types_in_public_api
import 'package:flutter/material.dart';
class ExpandedSection extends StatefulWidget {
final Widget child;
final Widget? child;
final bool expand;
double begin = 0.0;
double end = 1.0;
final double begin;
final double end;
ExpandedSection(
{this.expand = false,
required this.child,
required this.begin,
required this.end});
const ExpandedSection({
super.key,
this.expand = false,
this.child,
this.begin = 0.0,
this.end = 1.0,
});
@override
_ExpandedSectionState createState() => _ExpandedSectionState();