merge main

This commit is contained in:
guozhigq
2023-08-19 15:33:24 +08:00
208 changed files with 6302 additions and 2205 deletions

View File

@@ -7,3 +7,10 @@ class StyleString {
static const Radius imgRadius = Radius.circular(10);
static const double aspectRatio = 16 / 10;
}
class Constants {
static const String appKey = '27eb53fc9058f8c3';
static const String thirdSign = '04224646d1fea004e79606d3b038c84a';
static const String thirdApi =
'https://www.mcbbs.net/template/mcbbs/image/special_photo_bg.png';
}

View File

@@ -14,7 +14,7 @@ class VideoCardHSkeleton extends StatelessWidget {
child: LayoutBuilder(
builder: (context, boxConstraints) {
double width =
(boxConstraints.maxWidth - StyleString.cardSpace * 9) / 2;
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
return SizedBox(
height: width / StyleString.aspectRatio,
child: Row(

View File

@@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
class AnimatedDialog extends StatefulWidget {
const AnimatedDialog({Key? key, required this.child}) : super(key: key);
const AnimatedDialog({Key? key, required this.child, this.closeFn})
: super(key: key);
final Widget child;
final Function? closeFn;
@override
State<StatefulWidget> createState() => AnimatedDialogState();
@@ -39,12 +41,16 @@ class AnimatedDialogState extends State<AnimatedDialog>
Widget build(BuildContext context) {
return Material(
color: Colors.black.withOpacity(opacityAnimation!.value),
child: Center(
child: FadeTransition(
opacity: scaleAnimation!,
child: ScaleTransition(
scale: scaleAnimation!,
child: widget.child,
child: InkWell(
splashColor: Colors.transparent,
onTap: () => widget.closeFn!(),
child: Center(
child: FadeTransition(
opacity: scaleAnimation!,
child: ScaleTransition(
scale: scaleAnimation!,
child: widget.child,
),
),
),
),

View File

@@ -1,57 +0,0 @@
import 'package:flutter/material.dart';
class AppBarAni extends StatelessWidget implements PreferredSizeWidget {
const AppBarAni({
required this.child,
required this.controller,
required this.visible,
this.position,
Key? key,
}) : super(key: key);
final PreferredSizeWidget child;
final AnimationController controller;
final bool visible;
final String? position;
@override
Size get preferredSize => child.preferredSize;
@override
Widget build(BuildContext context) {
visible ? controller.reverse() : controller.forward();
return SlideTransition(
position: Tween<Offset>(
begin: Offset.zero,
end: Offset(0, position! == 'top' ? -1 : 1),
).animate(CurvedAnimation(
parent: controller,
curve: Curves.easeInOut,
)),
child: Container(
decoration: BoxDecoration(
gradient: position! == 'top'
? const LinearGradient(
begin: Alignment.bottomCenter,
end: Alignment.topCenter,
colors: <Color>[
Colors.transparent,
Colors.black45,
],
tileMode: TileMode.clamp,
)
: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[
Colors.transparent,
Colors.black45,
],
tileMode: TileMode.mirror,
),
),
child: child,
),
);
}
}

View File

@@ -1,33 +1,120 @@
import 'package:flutter/material.dart';
Widget pBadge(
text,
context,
double? top,
double? right,
double? bottom,
double? left, {
type = 'primary',
}) {
Color bgColor = Theme.of(context).colorScheme.primary;
Color color = Theme.of(context).colorScheme.onPrimary;
if (type == 'gray') {
bgColor = Colors.black54.withOpacity(0.4);
color = Colors.white;
}
return Positioned(
top: top,
left: left,
right: right,
bottom: bottom,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 6),
decoration:
BoxDecoration(borderRadius: BorderRadius.circular(4), color: bgColor),
child: Text(
text,
style: TextStyle(fontSize: 11, color: color),
// Widget pBadge(
// text,
// context,
// double? top,
// double? right,
// double? bottom,
// double? left, {
// type = 'primary',
// }) {
// Color bgColor = Theme.of(context).colorScheme.primary;
// Color color = Theme.of(context).colorScheme.onPrimary;
// if (type == 'gray') {
// bgColor = Colors.black54.withOpacity(0.4);
// color = Colors.white;
// }
// return Positioned(
// top: top,
// left: left,
// right: right,
// bottom: bottom,
// child: Container(
// padding: const EdgeInsets.symmetric(vertical: 1, horizontal: 6),
// decoration:
// BoxDecoration(borderRadius: BorderRadius.circular(4), color: bgColor),
// child: Text(
// text,
// style: TextStyle(fontSize: 11, color: color),
// ),
// ),
// );
// }
class PBadge extends StatelessWidget {
final String? text;
final double? top;
final double? right;
final double? bottom;
final double? left;
final String? type;
final String? size;
final String? stack;
final double? fs;
const PBadge({
super.key,
this.text,
this.top,
this.right,
this.bottom,
this.left,
this.type = 'primary',
this.size = 'medium',
this.stack = 'position',
this.fs = 11,
});
@override
Widget build(BuildContext context) {
ColorScheme t = Theme.of(context).colorScheme;
// 背景色
Color bgColor = t.primary;
// 前景色
Color color = t.onPrimary;
// 边框色
Color borderColor = Colors.transparent;
if (type == 'gray') {
bgColor = Colors.black54.withOpacity(0.4);
color = Colors.white;
}
if (type == 'color') {
bgColor = t.primaryContainer.withOpacity(0.6);
color = t.primary;
}
if (type == 'line') {
bgColor = Colors.transparent;
color = t.primary;
borderColor = t.primary;
}
EdgeInsets paddingStyle =
const EdgeInsets.symmetric(vertical: 1, horizontal: 6);
double fontSize = 11;
BorderRadius br = BorderRadius.circular(4);
if (size == 'small') {
paddingStyle = const EdgeInsets.symmetric(vertical: 0, horizontal: 3);
fontSize = 11;
br = BorderRadius.circular(3);
}
Widget content = Container(
padding: paddingStyle,
decoration: BoxDecoration(
borderRadius: br,
color: bgColor,
border: Border.all(color: borderColor),
),
),
);
child: Text(
text!,
style: TextStyle(fontSize: fs ?? fontSize, color: color),
),
);
if (stack == 'position') {
return Positioned(
top: top,
left: left,
right: right,
bottom: bottom,
child: content,
);
} else {
return Padding(
padding: const EdgeInsets.only(right: 5),
child: content,
);
}
}
}

View File

@@ -1,8 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/rcmd/controller.dart';
import 'package:pilipala/utils/utils.dart';
class LiveCard extends StatelessWidget {
@@ -95,7 +93,7 @@ class LiveContent extends StatelessWidget {
liveItem.title,
textAlign: TextAlign.start,
style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500),
maxLines: Get.find<RcmdController>().crossAxisCount,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(

View File

@@ -1,6 +1,10 @@
import 'package:flutter/material.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/utils/storage.dart';
Box setting = GStrorage.setting;
class NetworkImgLayer extends StatelessWidget {
final String? src;
@@ -24,12 +28,14 @@ class NetworkImgLayer extends StatelessWidget {
this.fadeOutDuration,
this.fadeInDuration,
// 图片质量 默认1%
this.quality = 1,
this.quality,
}) : super(key: key);
@override
Widget build(BuildContext context) {
double pr = MediaQuery.of(context).devicePixelRatio;
int picQuality = setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);
// double pr = 2;
return src != ''
? ClipRRect(
@@ -41,7 +47,7 @@ class NetworkImgLayer extends StatelessWidget {
: StyleString.imgRadius.x),
child: CachedNetworkImage(
imageUrl:
'${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality}q.webp',
'${src!.startsWith('//') ? 'https:${src!}' : src!}@${quality ?? picQuality}q.webp',
width: width ?? double.infinity,
height: height ?? double.infinity,
alignment: Alignment.center,

View File

@@ -1,42 +1,82 @@
import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/download.dart';
class OverlayPop extends StatelessWidget {
final dynamic videoItem;
const OverlayPop({super.key, this.videoItem});
final Function? closeFn;
const OverlayPop({super.key, this.videoItem, this.closeFn});
@override
Widget build(BuildContext context) {
double imgWidth = MediaQuery.of(context).size.width - 8 * 2;
return Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(6.0),
borderRadius: BorderRadius.circular(10.0),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(6.0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
NetworkImgLayer(
width: (MediaQuery.of(context).size.width - 16),
height: (MediaQuery.of(context).size.width - 16) /
StyleString.aspectRatio,
src: videoItem.pic!,
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 15, 10, 15),
child: Text(
videoItem.title!,
// maxLines: 1,
// overflow: TextOverflow.ellipsis,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Stack(
children: [
NetworkImgLayer(
width: imgWidth,
height: imgWidth / StyleString.aspectRatio,
src: videoItem.pic!,
quality: 100,
),
),
],
),
Positioned(
right: 8,
top: 8,
child: Container(
width: 30,
height: 30,
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.3),
borderRadius:
const BorderRadius.all(Radius.circular(20))),
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () => closeFn!(),
icon: const Icon(
Icons.close,
size: 18,
color: Colors.white,
),
),
),
),
],
),
Padding(
padding: const EdgeInsets.fromLTRB(12, 10, 8, 10),
child: Row(
children: [
Expanded(
child: Text(
videoItem.title!,
),
),
const SizedBox(width: 4),
IconButton(
tooltip: '保存封面图',
onPressed: () async {
await DownloadUtils.downloadImg(
videoItem.pic ?? videoItem.cover);
// closeFn!();
},
icon: const Icon(Icons.download, size: 20),
)
],
)),
],
),
);
}

View File

@@ -1,24 +0,0 @@
import 'package:flutter/material.dart';
class UpTag extends StatelessWidget {
const UpTag({super.key});
@override
Widget build(BuildContext context) {
return Container(
width: 14,
height: 10,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(2),
border: Border.all(color: Theme.of(context).colorScheme.outline)),
margin: const EdgeInsets.only(right: 4),
child: Center(
child: Text(
'UP',
style: TextStyle(
fontSize: 6, color: Theme.of(context).colorScheme.outline),
),
),
);
}
}

View File

@@ -16,12 +16,14 @@ class VideoCardH extends StatelessWidget {
final videoItem;
final Function()? longPress;
final Function()? longPressEnd;
final String source;
const VideoCardH({
Key? key,
required this.videoItem,
this.longPress,
this.longPressEnd,
this.source = 'normal',
}) : super(key: key);
@override
@@ -35,11 +37,11 @@ class VideoCardH extends StatelessWidget {
longPress!();
}
},
onLongPressEnd: (details) {
if (longPressEnd != null) {
longPressEnd!();
}
},
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async {
try {
@@ -57,8 +59,9 @@ class VideoCardH extends StatelessWidget {
child: LayoutBuilder(
builder: (context, boxConstraints) {
double width =
(boxConstraints.maxWidth - StyleString.cardSpace * 9) / 2;
return SizedBox(
(boxConstraints.maxWidth - StyleString.cardSpace * 6) / 2;
return Container(
constraints: const BoxConstraints(minHeight: 88),
height: width / StyleString.aspectRatio,
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
@@ -80,19 +83,24 @@ class VideoCardH extends StatelessWidget {
height: maxHeight,
),
),
pBadge(Utils.timeFormat(videoItem.duration!),
context, null, 6.0, 6.0, null,
type: 'gray'),
if (videoItem.rcmdReason != null &&
videoItem.rcmdReason.content != '')
pBadge(videoItem.rcmdReason.content, context,
6.0, 6.0, null, null),
PBadge(
text: Utils.timeFormat(videoItem.duration!),
top: null,
right: 6.0,
bottom: 6.0,
left: null,
type: 'gray',
),
// if (videoItem.rcmdReason != null &&
// videoItem.rcmdReason.content != '')
// pBadge(videoItem.rcmdReason.content, context,
// 6.0, 6.0, null, null),
],
);
},
),
),
VideoContent(videoItem: videoItem)
VideoContent(videoItem: videoItem, source: source)
],
),
);
@@ -107,7 +115,9 @@ class VideoCardH extends StatelessWidget {
class VideoContent extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables
final videoItem;
const VideoContent({super.key, required this.videoItem});
final String source;
const VideoContent(
{super.key, required this.videoItem, this.source = 'normal'});
@override
Widget build(BuildContext context) {
@@ -124,7 +134,6 @@ class VideoContent extends StatelessWidget {
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
@@ -198,26 +207,62 @@ class VideoContent extends StatelessWidget {
// color: Theme.of(context).colorScheme.outline),
// )
const Spacer(),
SizedBox(
width: 20,
height: 20,
child: IconButton(
tooltip: '稍后再看',
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () async {
var res =
await UserHttp.toViewLater(bvid: videoItem.bvid);
SmartDialog.showToast(res['msg']);
},
icon: Icon(
Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline,
size: 14,
// SizedBox(
// width: 20,
// height: 20,
// child: IconButton(
// tooltip: '稍后再看',
// style: ButtonStyle(
// padding: MaterialStateProperty.all(EdgeInsets.zero),
// ),
// onPressed: () async {
// var res =
// await UserHttp.toViewLater(bvid: videoItem.bvid);
// SmartDialog.showToast(res['msg']);
// },
// icon: Icon(
// Icons.more_vert_outlined,
// color: Theme.of(context).colorScheme.outline,
// size: 14,
// ),
// ),
// ),
if (source == 'normal')
SizedBox(
width: 24,
height: 24,
child: PopupMenuButton<String>(
padding: EdgeInsets.zero,
tooltip: '稍后再看',
icon: Icon(
Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline,
size: 14,
),
position: PopupMenuPosition.under,
// constraints: const BoxConstraints(maxHeight: 35),
onSelected: (String type) {},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
onTap: () async {
var res = await UserHttp.toViewLater(
bvid: videoItem.bvid);
SmartDialog.showToast(res['msg']);
},
value: 'pause',
height: 35,
child: const Row(
children: [
Icon(Icons.watch_later_outlined, size: 16),
SizedBox(width: 6),
Text('稍后再看', style: TextStyle(fontSize: 13))
],
),
),
],
),
),
),
],
),
],

View File

@@ -2,18 +2,19 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/stat/danmu.dart';
import 'package:pilipala/common/widgets/stat/view.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/pages/rcmd/index.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
// 视频卡片 - 垂直布局
class VideoCardV extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables
final videoItem;
final dynamic videoItem;
final Function()? longPress;
final Function()? longPressEnd;
@@ -24,6 +25,54 @@ class VideoCardV extends StatelessWidget {
this.longPressEnd,
}) : super(key: key);
void onPushDetail(heroTag) async {
String goto = videoItem.goto;
switch (goto) {
case 'bangumi':
if (videoItem.bangumiBadge == '电影') {
SmartDialog.showToast('暂不支持电影观看');
return;
}
int epId = videoItem.param;
SmartDialog.showLoading(msg: '资源获取中');
var result = await SearchHttp.bangumiInfo(seasonId: null, epId: epId);
if (result['status']) {
var bangumiDetail = result['data'];
int cid = bangumiDetail.episodes!.first.cid;
String bvid = IdUtils.av2bv(bangumiDetail.episodes!.first.aid);
SmartDialog.dismiss().then(
(value) => Get.toNamed(
'/video?bvid=$bvid&cid=$cid&epId=$epId',
arguments: {
'pic': videoItem.pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
},
),
);
}
break;
case 'av':
String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid);
Get.toNamed('/video?bvid=$bvid&cid=${videoItem.cid}', arguments: {
// 'videoItem': videoItem,
'pic': videoItem.pic,
'heroTag': heroTag,
});
break;
default:
SmartDialog.showToast(videoItem.goto);
Get.toNamed(
'/webview',
parameters: {
'url': videoItem.uri,
'type': 'url',
'pageTitle': videoItem.title,
},
);
}
}
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(videoItem.id);
@@ -40,61 +89,29 @@ class VideoCardV extends StatelessWidget {
longPress!();
}
},
onLongPressEnd: (details) {
if (longPressEnd != null) {
longPressEnd!();
}
},
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async {
String bvid = videoItem.bvid ?? IdUtils.av2bv(videoItem.aid);
Get.toNamed('/video?bvid=$bvid&cid=${videoItem.cid}',
arguments: {'videoItem': videoItem, 'heroTag': heroTag});
},
onTap: () async => onPushDetail(heroTag),
child: Column(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: StyleString.imgRadius,
topRight: StyleString.imgRadius,
bottomLeft: StyleString.imgRadius,
bottomRight: StyleString.imgRadius,
),
child: AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: videoItem.pic,
width: maxWidth,
height: maxHeight,
),
),
// if (videoItem.stat.view is int &&
// videoItem.stat.danmaku is int)
// Positioned(
// left: 0,
// right: 0,
// bottom: 0,
// child: AnimatedOpacity(
// opacity: 1,
// duration: const Duration(milliseconds: 200),
// child: VideoStat(
// view: videoItem.stat.view,
// danmaku: videoItem.stat.danmaku,
// duration: videoItem.duration,
// ),
// ),
// ),
],
);
}),
),
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Hero(
tag: heroTag,
child: NetworkImgLayer(
src: videoItem.pic,
width: maxWidth,
height: maxHeight,
),
);
}),
),
VideoContent(videoItem: videoItem)
],
@@ -106,113 +123,151 @@ class VideoCardV extends StatelessWidget {
}
class VideoContent extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables
final videoItem;
final dynamic videoItem;
const VideoContent({Key? key, required this.videoItem}) : super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
child: Padding(
// 多列
padding: const EdgeInsets.fromLTRB(4, 5, 0, 3),
// 单列
// padding: const EdgeInsets.fromLTRB(14, 10, 4, 8),
padding: const EdgeInsets.fromLTRB(4, 8, 0, 3),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
videoItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
maxLines: Get.find<RcmdController>().crossAxisCount,
style: const TextStyle(fontSize: 13),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
Row(
children: [
if (videoItem.goto == 'bangumi') ...[
PBadge(
text: videoItem.bangumiBadge,
stack: 'normal',
size: 'small',
type: 'line',
fs: 9,
)
],
if (videoItem.rcmdReason != null &&
videoItem.rcmdReason.content != '' ||
videoItem.isFollowed == 1) ...[
Container(
padding: const EdgeInsets.fromLTRB(3, 0, 3, 0),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.6),
borderRadius: BorderRadius.circular(3)),
child: Center(
child: Text(
videoItem.rcmdReason != null &&
videoItem.rcmdReason.content != ''
? videoItem.rcmdReason.content
: '已关注',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize,
color: Theme.of(context).colorScheme.primary,
),
),
)),
const SizedBox(width: 4)
videoItem.rcmdReason.content != '') ...[
PBadge(
text: videoItem.rcmdReason.content,
stack: 'normal',
size: 'small',
type: 'color',
)
],
if (videoItem.goto == 'picture') ...[
const PBadge(
text: '动态',
stack: 'normal',
size: 'small',
type: 'line',
fs: 9,
)
],
Expanded(
child: LayoutBuilder(builder:
(BuildContext context, BoxConstraints constraints) {
return SizedBox(
width: constraints.maxWidth,
child: Text(
videoItem.owner.name,
maxLines: 1,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
);
}),
),
SizedBox(
width: 20,
height: 20,
child: IconButton(
tooltip: '稍后再看',
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () async {
var res =
await UserHttp.toViewLater(bvid: videoItem.bvid);
SmartDialog.showToast(res['msg']);
},
icon: Icon(
Icons.more_vert_outlined,
child: Text(
videoItem.owner.name,
maxLines: 1,
style: TextStyle(
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
size: 14,
),
),
),
if (videoItem.goto == 'av')
SizedBox(
width: 24,
height: 24,
child: PopupMenuButton<String>(
padding: EdgeInsets.zero,
tooltip: '稍后再看',
icon: Icon(
Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline,
size: 14,
),
position: PopupMenuPosition.under,
// constraints: const BoxConstraints(maxHeight: 35),
onSelected: (String type) {},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
onTap: () async {
int aid = videoItem.param;
var res = await UserHttp.toViewLater(
bvid: IdUtils.av2bv(aid));
SmartDialog.showToast(res['msg']);
},
value: 'pause',
height: 35,
child: const Row(
children: [
Icon(Icons.watch_later_outlined, size: 16),
SizedBox(width: 6),
Text('稍后再看', style: TextStyle(fontSize: 13))
],
),
),
],
),
),
],
),
// Row(
// children: [
// const SizedBox(width: 1),
// StatView(
// theme: 'black',
// theme: 'gray',
// view: videoItem.stat.view,
// ),
// const SizedBox(width: 6),
// const SizedBox(width: 10),
// StatDanMu(
// theme: 'black',
// theme: 'gray',
// danmu: videoItem.stat.danmaku,
// ),
// const Spacer(),
// SizedBox(
// width: 24,
// height: 24,
// child: PopupMenuButton<String>(
// padding: EdgeInsets.zero,
// tooltip: '稍后再看',
// icon: Icon(
// Icons.more_vert_outlined,
// color: Theme.of(context).colorScheme.outline,
// size: 14,
// ),
// position: PopupMenuPosition.under,
// // constraints: const BoxConstraints(maxHeight: 35),
// onSelected: (String type) {},
// itemBuilder: (BuildContext context) =>
// <PopupMenuEntry<String>>[
// PopupMenuItem<String>(
// onTap: () async {
// var res =
// await UserHttp.toViewLater(bvid: videoItem.bvid);
// SmartDialog.showToast(res['msg']);
// },
// value: 'pause',
// height: 35,
// child: const Row(
// children: [
// Icon(Icons.watch_later_outlined, size: 16),
// SizedBox(width: 6),
// Text('稍后再看', style: TextStyle(fontSize: 13))
// ],
// ),
// ),
// ],
// ),
// ),
// ],
// ),
],
@@ -237,7 +292,7 @@ class VideoStat extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 45,
height: 48,
padding: const EdgeInsets.only(top: 22, left: 6, right: 6),
decoration: const BoxDecoration(
gradient: LinearGradient(

View File

@@ -248,9 +248,44 @@ class Api {
// 移除已观看
static const String toViewDel = '/x/v2/history/toview/del';
// 清空稍后再看
static const String toViewClear = '/x/v2/history/toview/clear';
// 追番
static const String bangumiAdd = '/pgc/web/follow/add';
// 取消追番
static const String bangumiDel = '/pgc/web/follow/del';
// 番剧列表
// https://api.bilibili.com/pgc/season/index/result?
// st=1&
// order=3
// season_version=-1 全部-1 正片1 电影2 其他3
// spoken_language_type=-1 全部-1 原生1 中文配音2
// area=-1&
// is_finish=-1&
// copyright=-1&
// season_status=-1&
// season_month=-1&
// year=-1&
// style_id=-1&
// sort=0&
// page=1&
// season_type=1&
// pagesize=20&
// type=1
static const String bangumiList =
'/pgc/season/index/result?st=1&order=3&season_version=-1&spoken_language_type=-1&area=-1&is_finish=-1&copyright=-1&season_status=-1&season_month=-1&year=-1&style_id=-1&sort=0&season_type=1&pagesize=20&type=1';
// 我的订阅
static const String bangumiFollow =
'/x/space/bangumi/follow/list?type=1&follow_status=0&pn=1&ps=15&ts=1691544359969';
// 黑名单
static const String blackLst = '/x/relation/blacks';
// github 获取最新版
static const String latestApp =
'https://api.github.com/repos/guozhigq/pilipala/releases/latest';
}

36
lib/http/bangumi.dart Normal file
View File

@@ -0,0 +1,36 @@
import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/bangumi/list.dart';
class BangumiHttp {
static Future bangumiList({int? page}) async {
var res = await Request().get(Api.bangumiList, data: {'page': page});
if (res.data['code'] == 0) {
return {
'status': true,
'data': BangumiListDataModel.fromJson(res.data['data'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
static Future bangumiFollow({int? mid}) async {
var res = await Request().get(Api.bangumiFollow, data: {'vmid': mid});
if (res.data['code'] == 0) {
return {
'status': true,
'data': BangumiListDataModel.fromJson(res.data['data'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

26
lib/http/black.dart Normal file
View File

@@ -0,0 +1,26 @@
import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/user/black.dart';
class BlackHttp {
static Future blackList({required int pn, int? ps}) async {
var res = await Request().get(Api.blackLst, data: {
'pn': pn,
'ps': ps ?? 50,
're_version': 0,
'jsonp': 'jsonp',
'csrf': await Request.getCsrf(),
});
if (res.data['code'] == 0) {
return {
'status': true,
'data': BlackListDataModel.fromJson(res.data['data'])
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

View File

@@ -15,20 +15,12 @@ import 'package:dio_cookie_manager/dio_cookie_manager.dart';
class Request {
static final Request _instance = Request._internal();
static late CookieManager cookieManager;
static late final Dio dio;
factory Request() => _instance;
static Dio dio = Dio()
..httpClientAdapter = Http2Adapter(
ConnectionManager(
idleTimeout: const Duration(milliseconds: 10000),
// Ignore bad certificate
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
),
);
/// 设置cookie
static setCookie() async {
Box user = GStrorage.user;
var cookiePath = await Utils.getCookiePath();
var cookieJar = PersistCookieJar(
ignoreExpires: true,
@@ -38,8 +30,18 @@ class Request {
dio.interceptors.add(cookieManager);
var cookie = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.baseUrl));
var cookie2 = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.tUrl));
if (user.get(UserBoxKey.userMid) != null) {
var cookie2 = await cookieManager.cookieJar
.loadForRequest(Uri.parse(HttpString.tUrl));
if (cookie2.isEmpty) {
try {
await Request().get(HttpString.tUrl);
} catch (e) {
log("setCookie, ${e.toString()}");
}
}
}
if (cookie.isEmpty) {
try {
await Request().get(HttpString.baseUrl);
@@ -47,23 +49,9 @@ class Request {
log("setCookie, ${e.toString()}");
}
}
if (cookie2.isEmpty) {
try {
await Request().get(HttpString.tUrl);
} catch (e) {
log("setCookie, ${e.toString()}");
}
}
}
// 移除cookie
static removeCookie() async {
await cookieManager.cookieJar
.saveFromResponse(Uri.parse(HttpString.baseUrl), []);
await cookieManager.cookieJar
.saveFromResponse(Uri.parse(HttpString.baseApiUrl), []);
cookieManager.cookieJar.deleteAll();
dio.interceptors.add(cookieManager);
var cookieString =
cookie.map((cookie) => '${cookie.name}=${cookie.value}').join('; ');
dio.options.headers['cookie'] = cookieString;
}
// 从cookie中获取 csrf token
@@ -95,28 +83,38 @@ class Request {
//Http请求头.
headers: {
// 'cookie': '',
"env": 'prod',
"app-key": 'android',
"x-bili-aurora-eid": 'UlMFQVcABlAH',
"x-bili-aurora-zone": 'sh001',
'referer': 'https://www.bilibili.com/',
},
);
Box user = GStrorage.user;
if (user.get(UserBoxKey.userMid) != null) {
options.headers['x-bili-mid'] = user.get(UserBoxKey.userMid).toString();
options.headers['env'] = 'prod';
options.headers['app-key'] = 'android64';
options.headers['x-bili-aurora-eid'] = 'UlMFQVcABlAH';
options.headers['x-bili-aurora-zone'] = 'sh001';
options.headers['referer'] = 'https://www.bilibili.com/';
}
dio.options = options;
dio = Dio(options)
..httpClientAdapter = Http2Adapter(
ConnectionManager(
idleTimeout: const Duration(milliseconds: 10000),
// Ignore bad certificate
onClientCreate: (_, config) => config.onBadCertificate = (_) => true,
),
);
//添加拦截器
dio.interceptors
..add(ApiInterceptor())
// 日志拦截器 输出请求、响应内容
..add(LogInterceptor(
request: false,
requestHeader: false,
responseHeader: false,
));
dio.interceptors.add(ApiInterceptor());
// 日志拦截器 输出请求、响应内容
dio.interceptors.add(LogInterceptor(
request: false,
requestHeader: false,
responseHeader: false,
));
dio.transformer = BackgroundTransformer();
dio.options.validateStatus = (status) {
return status! >= 200 && status < 300 || status == 304 || status == 302;
@@ -161,7 +159,7 @@ class Request {
* post请求
*/
post(url, {data, queryParameters, options, cancelToken, extra}) async {
print('post-data: $data');
// print('post-data: $data');
Response response;
try {
response = await dio.post(
@@ -171,7 +169,7 @@ class Request {
options: options,
cancelToken: cancelToken,
);
print('post success: ${response.data}');
// print('post success: ${response.data}');
return response;
} on DioException catch (e) {
print('post error: $e');

View File

@@ -1,6 +1,10 @@
// ignore_for_file: avoid_print
import 'package:dio/dio.dart';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/utils/storage.dart';
// import 'package:get/get.dart' hide Response;
class ApiInterceptor extends Interceptor {
@@ -13,8 +17,26 @@ class ApiInterceptor extends Interceptor {
handler.next(options);
}
Box user = GStrorage.user;
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
try {
if (response.statusCode == 302) {
List<String> locations = response.headers['location']!;
if (locations.isNotEmpty) {
if (locations.first.startsWith('https://www.mcbbs.net')) {
final uri = Uri.parse(locations.first);
final accessKey = uri.queryParameters['access_key'];
final mid = uri.queryParameters['mid'];
user.put(UserBoxKey.accessKey, {'mid': mid, 'value': accessKey});
}
}
}
} catch (err) {
print('ApiInterceptor: $err');
}
handler.next(response);
}

View File

@@ -1,3 +1,4 @@
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/model_hot_video_item.dart';
@@ -84,6 +85,12 @@ class UserHttp {
static Future<dynamic> seeYouLater() async {
var res = await Request().get(Api.seeYouLater);
if (res.data['code'] == 0) {
if (res.data['data']['count'] == 0) {
return {
'status': true,
'data': {'list': [], 'count': 0}
};
}
List<HotVideoItemModel> list = [];
for (var i in res.data['data']['list']) {
list.add(HotVideoItemModel.fromJson(i));
@@ -179,4 +186,35 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']};
}
}
// 获取用户凭证
static Future thirdLogin() async {
var res = await Request().get(
'https://passport.bilibili.com/login/app/third',
data: {
'appkey': Constants.appKey,
'api': Constants.thirdApi,
'sign': Constants.thirdSign,
},
);
if (res.data['code'] == 0 && res.data['data']['has_login'] == 1) {
Request().get(res.data['data']['confirm_uri']);
}
}
// 清空稍后再看
static Future toViewClear() async {
var res = await Request().post(
Api.toViewClear,
queryParameters: {
'jsonp': 'jsonp',
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {'status': true, 'msg': '操作完成'};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
}

View File

@@ -1,5 +1,7 @@
import 'dart:developer';
import 'package:hive/hive.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/http/api.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/common/reply_type.dart';
@@ -9,12 +11,16 @@ import 'package:pilipala/models/model_rec_video_item.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/utils/storage.dart';
/// res.data['code'] == 0 请求正常返回结果
/// res.data['data'] 为结果
/// 返回{'status': bool, 'data': List}
/// view层根据 status 判断渲染逻辑
class VideoHttp {
static Box user = GStrorage.user;
static Box setting = GStrorage.setting;
// 首页推荐视频
static Future rcmdVideoList({required int ps, required int freshIdx}) async {
try {
@@ -42,8 +48,7 @@ class VideoHttp {
}
}
static Future rcmdVideoListApp(
{required int ps, required int freshIdx}) async {
static Future rcmdVideoListApp({int? ps, required int freshIdx}) async {
try {
var res = await Request().get(
Api.recommendListApp,
@@ -55,12 +60,22 @@ class VideoHttp {
'device_type': 0,
'device_name': 'vivo',
'pull': freshIdx == 0 ? 'true' : 'false',
'appkey': Constants.appKey,
'access_key':
user.get(UserBoxKey.accessKey, defaultValue: {})['value'] ?? ''
},
);
if (res.data['code'] == 0) {
List<RecVideoItemAppModel> list = [];
List<int> blackMidsList =
setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
for (var i in res.data['data']['items']) {
list.add(RecVideoItemAppModel.fromJson(i));
// 屏蔽推广和拉黑用户
if (i['card_goto'] != 'ad_av' &&
(i['args'] != null &&
!blackMidsList.contains(i['args']['up_mid']))) {
list.add(RecVideoItemAppModel.fromJson(i));
}
}
return {'status': true, 'data': list};
} else {
@@ -80,8 +95,12 @@ class VideoHttp {
);
if (res.data['code'] == 0) {
List<HotVideoItemModel> list = [];
List<int> blackMidsList =
setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
for (var i in res.data['data']['list']) {
list.add(HotVideoItemModel.fromJson(i));
if (!blackMidsList.contains(i['owner']['mid'])) {
list.add(HotVideoItemModel.fromJson(i));
}
}
return {'status': true, 'data': list};
} else {

View File

@@ -4,8 +4,10 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/custom_toast.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/router/app_pages.dart';
@@ -33,19 +35,38 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
Color brandColor = const Color.fromARGB(255, 92, 182, 123);
Box setting = GStrorage.setting;
ThemeType currentThemeValue = ThemeType.values[setting
.get(SettingBoxKey.themeMode, defaultValue: ThemeType.system.code)];
return DynamicColorBuilder(
builder: ((lightDynamic, darkDynamic) {
builder: ((ColorScheme? lightDynamic, ColorScheme? darkDynamic) {
ColorScheme? lightColorScheme;
ColorScheme? darkColorScheme;
if (lightDynamic != null && darkDynamic != null) {
// dynamic取色成功
lightColorScheme = lightDynamic.harmonized();
darkColorScheme = darkDynamic.harmonized();
} else {
// dynamic取色失败采用品牌色
lightColorScheme = ColorScheme.fromSeed(
seedColor: brandColor,
brightness: Brightness.light,
);
darkColorScheme = ColorScheme.fromSeed(
seedColor: brandColor,
brightness: Brightness.dark,
);
}
// 图片缓存
// PaintingBinding.instance.imageCache.maximumSizeBytes = 1000 << 20;
return GetMaterialApp(
title: 'PiLiPaLa',
theme: ThemeData(
fontFamily: 'HarmonyOS',
colorScheme: lightDynamic ??
ColorScheme.fromSeed(
seedColor: Colors.green,
brightness: Brightness.light,
),
// fontFamily: 'HarmonyOS',
colorScheme: currentThemeValue == ThemeType.dark
? darkColorScheme
: lightColorScheme,
useMaterial3: true,
pageTransitionsTheme: const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
@@ -56,12 +77,10 @@ class MyApp extends StatelessWidget {
),
),
darkTheme: ThemeData(
fontFamily: 'HarmonyOS',
colorScheme: darkDynamic ??
ColorScheme.fromSeed(
seedColor: Colors.green,
brightness: Brightness.dark,
),
// fontFamily: 'HarmonyOS',
colorScheme: currentThemeValue == ThemeType.light
? lightColorScheme
: darkColorScheme,
useMaterial3: true,
),
localizationsDelegates: const [

View File

@@ -0,0 +1,90 @@
class BangumiListDataModel {
BangumiListDataModel({
this.hasNext,
this.list,
this.num,
this.size,
this.total,
});
int? hasNext;
List? list;
int? num;
int? size;
int? total;
BangumiListDataModel.fromJson(Map<String, dynamic> json) {
hasNext = json['has_next'];
list = json['list'] != null
? json['list']
.map<BangumiListItemModel>((e) => BangumiListItemModel.fromJson(e))
.toList()
: [];
num = json['num'];
size = json['size'];
total = json['total'];
}
}
class BangumiListItemModel {
BangumiListItemModel({
this.badge,
this.badgeType,
this.cover,
// this.firstEp,
this.indexShow,
this.isFinish,
this.link,
this.mediaId,
this.order,
this.orderType,
this.score,
this.seasonId,
this.seaconStatus,
this.seasonType,
this.subTitle,
this.title,
this.titleIcon,
this.progress,
});
String? badge;
int? badgeType;
String? cover;
String? indexShow;
int? isFinish;
String? link;
int? mediaId;
String? order;
String? orderType;
String? score;
int? seasonId;
int? seaconStatus;
int? seasonType;
String? subTitle;
String? title;
String? titleIcon;
String? progress;
BangumiListItemModel.fromJson(Map<String, dynamic> json) {
badge = json['badge'] == '' ? null : json['badge'];
badgeType = json['badge_type'];
cover = json['cover'];
indexShow = json['index_show'];
isFinish = json['is_finish'];
link = json['link'];
mediaId = json['media_id'];
order = json['order'];
orderType = json['order_type'];
score = json['score'];
seasonId = json['season_id'];
seaconStatus = json['seacon_status'];
seasonType = json['season_type'];
subTitle = json['sub_title'];
title = json['title'];
titleIcon = json['title_icon'];
progress = json['progress'];
}
}

View File

@@ -0,0 +1,55 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/pages/bangumi/index.dart';
import 'package:pilipala/pages/hot/index.dart';
import 'package:pilipala/pages/live/index.dart';
import 'package:pilipala/pages/rcmd/index.dart';
enum TabType { live, rcmd, hot, bangumi }
extension TabTypeDesc on TabType {
String get description => ['直播', '推荐', '热门', '番剧'][index];
}
List tabsConfig = [
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '直播',
'type': TabType.live,
'ctr': Get.find<LiveController>,
'page': const LivePage(),
},
{
'icon': const Icon(
Icons.thumb_up_off_alt_outlined,
size: 15,
),
'label': '推荐',
'type': TabType.rcmd,
'ctr': Get.find<RcmdController>,
'page': const RcmdPage(),
},
{
'icon': const Icon(
Icons.whatshot_outlined,
size: 15,
),
'label': '热门',
'type': TabType.hot,
'ctr': Get.find<HotController>,
'page': const HotPage(),
},
{
'icon': const Icon(
Icons.play_circle_outlined,
size: 15,
),
'label': '番剧',
'type': TabType.bangumi,
'ctr': Get.find<BangumiController>,
'page': const BangumiPage(),
},
];

View File

@@ -0,0 +1,13 @@
enum ThemeType {
light,
dark,
system,
}
extension ThemeTypeDesc on ThemeType {
String get description => ['浅色', '深色', '跟随系统'][index];
}
extension ThemeTypeCode on ThemeType {
int get code => [0, 1, 2][index];
}

View File

@@ -478,6 +478,8 @@ class DynamicArchiveModel {
this.stat,
this.title,
this.type,
this.epid,
this.seasonId,
});
int? aid;
@@ -491,6 +493,8 @@ class DynamicArchiveModel {
Stat? stat;
String? title;
int? type;
int? epid;
int? seasonId;
DynamicArchiveModel.fromJson(Map<String, dynamic> json) {
aid = json['aid'] is String ? int.parse(json['aid']) : json['aid'];
@@ -503,6 +507,8 @@ class DynamicArchiveModel {
stat = json['stat'] != null ? Stat.fromJson(json['stat']) : null;
title = json['title'];
type = json['type'];
epid = json['epid'];
seasonId = json['season_id'];
}
}

View File

@@ -0,0 +1,45 @@
class LatestDataModel {
LatestDataModel({
this.url,
this.tagName,
this.createdAt,
this.assets,
});
String? url;
String? tagName;
String? createdAt;
List? assets;
LatestDataModel.fromJson(Map<String, dynamic> json) {
url = json['url'];
tagName = json['tag_name'];
createdAt = json['created_at'];
assets =
json['assets'].map<AssetItem>((e) => AssetItem.fromJson(e)).toList();
}
}
class AssetItem {
AssetItem({
this.url,
this.name,
this.size,
this.downloadCount,
this.downloadUrl,
});
String? url;
String? name;
int? size;
int? downloadCount;
String? downloadUrl;
AssetItem.fromJson(Map<String, dynamic> json) {
url = json['url'];
name = json['name'];
size = json['size'];
downloadCount = json['download_count'];
downloadUrl = json['browser_download_url'];
}
}

View File

@@ -1,3 +1,8 @@
import 'package:hive/hive.dart';
part 'result.g.dart';
@HiveType(typeId: 0)
class RecVideoItemAppModel {
RecVideoItemAppModel({
this.id,
@@ -11,55 +16,141 @@ class RecVideoItemAppModel {
this.isFollowed,
this.owner,
this.rcmdReason,
this.goto,
this.param,
this.uri,
this.talkBack,
this.bangumiView,
this.bangumiFollow,
this.bangumiBadge,
this.cardType,
this.adInfo,
});
@HiveField(0)
int? id;
@HiveField(1)
int? aid;
int? bvid;
@HiveField(2)
String? bvid;
@HiveField(3)
int? cid;
@HiveField(4)
String? pic;
Stat? stat;
int? duration;
@HiveField(5)
RcmdStat? stat;
@HiveField(6)
String? duration;
@HiveField(7)
String? title;
@HiveField(8)
int? isFollowed;
Owner? owner;
String? rcmdReason;
@HiveField(9)
RcmdOwner? owner;
@HiveField(10)
RcmdReason? rcmdReason;
@HiveField(11)
String? goto;
@HiveField(12)
int? param;
@HiveField(13)
String? uri;
@HiveField(14)
String? talkBack;
// 番剧
@HiveField(15)
String? bangumiView;
@HiveField(16)
String? bangumiFollow;
@HiveField(17)
String? bangumiBadge;
@HiveField(18)
String? cardType;
@HiveField(19)
Map? adInfo;
RecVideoItemAppModel.fromJson(Map<String, dynamic> json) {
id = json['player_args']['aid'];
aid = json['player_args']['aid'];
cid = json['player_args']['cid'];
id = json['player_args'] != null
? json['player_args']['aid']
: int.parse(json['param'] ?? '-1');
aid = json['player_args'] != null ? json['player_args']['aid'] : -1;
bvid = null;
cid = json['player_args'] != null ? json['player_args']['cid'] : -1;
pic = json['cover'];
stat = Stat.fromJson(json);
duration = json['player_args']['duration'];
stat = RcmdStat.fromJson(json);
duration = json['cover_right_text'];
title = json['title'];
isFollowed = 0;
owner = Owner.fromJson(json);
owner = RcmdOwner.fromJson(json);
rcmdReason = json['rcmd_reason_style'] != null
? RcmdReason.fromJson(json['rcmd_reason_style'])
: null;
goto = json['goto'];
param = int.parse(json['param']);
uri = json['uri'];
talkBack = json['talk_back'];
if (json['goto'] == 'bangumi') {
bangumiView = json['cover_left_text_1'];
bangumiFollow = json['cover_left_text_2'];
bangumiBadge = json['badge'];
}
cardType = json['card_type'];
adInfo = json['ad_info'];
}
}
class Stat {
Stat({
@HiveType(typeId: 1)
class RcmdStat {
RcmdStat({
this.view,
this.like,
this.danmaku,
this.danmu,
});
@HiveField(0)
String? view;
@HiveField(1)
String? like;
String? danmaku;
@HiveField(2)
String? danmu;
Stat.fromJson(Map<String, dynamic> json) {
RcmdStat.fromJson(Map<String, dynamic> json) {
view = json["cover_left_text_1"];
danmaku = json['cover_left_text_2'];
danmu = json['cover_left_text_2'];
}
}
class Owner {
Owner({this.name});
@HiveType(typeId: 2)
class RcmdOwner {
RcmdOwner({this.name, this.mid});
@HiveField(0)
String? name;
@HiveField(1)
int? mid;
Owner.fromJson(Map<String, dynamic> json) {
name = json['args']['up_name'];
RcmdOwner.fromJson(Map<String, dynamic> json) {
name = json['goto'] == 'av'
? json['args']['up_name']
: json['desc_button'] != null
? json['desc_button']['text']
: '';
mid = json['args']['up_id'] ?? -1;
}
}
@HiveType(typeId: 8)
class RcmdReason {
RcmdReason({
this.content,
});
@HiveField(0)
String? content;
RcmdReason.fromJson(Map<String, dynamic> json) {
content = json["text"] ?? '';
}
}

View File

@@ -0,0 +1,209 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'result.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class RecVideoItemAppModelAdapter extends TypeAdapter<RecVideoItemAppModel> {
@override
final int typeId = 0;
@override
RecVideoItemAppModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RecVideoItemAppModel(
id: fields[0] as int?,
aid: fields[1] as int?,
bvid: fields[2] as String?,
cid: fields[3] as int?,
pic: fields[4] as String?,
stat: fields[5] as RcmdStat?,
duration: fields[6] as String?,
title: fields[7] as String?,
isFollowed: fields[8] as int?,
owner: fields[9] as RcmdOwner?,
rcmdReason: fields[10] as RcmdReason?,
goto: fields[11] as String?,
param: fields[12] as int?,
uri: fields[13] as String?,
talkBack: fields[14] as String?,
bangumiView: fields[15] as String?,
bangumiFollow: fields[16] as String?,
bangumiBadge: fields[17] as String?,
cardType: fields[18] as String?,
adInfo: (fields[19] as Map?)?.cast<dynamic, dynamic>(),
);
}
@override
void write(BinaryWriter writer, RecVideoItemAppModel obj) {
writer
..writeByte(20)
..writeByte(0)
..write(obj.id)
..writeByte(1)
..write(obj.aid)
..writeByte(2)
..write(obj.bvid)
..writeByte(3)
..write(obj.cid)
..writeByte(4)
..write(obj.pic)
..writeByte(5)
..write(obj.stat)
..writeByte(6)
..write(obj.duration)
..writeByte(7)
..write(obj.title)
..writeByte(8)
..write(obj.isFollowed)
..writeByte(9)
..write(obj.owner)
..writeByte(10)
..write(obj.rcmdReason)
..writeByte(11)
..write(obj.goto)
..writeByte(12)
..write(obj.param)
..writeByte(13)
..write(obj.uri)
..writeByte(14)
..write(obj.talkBack)
..writeByte(15)
..write(obj.bangumiView)
..writeByte(16)
..write(obj.bangumiFollow)
..writeByte(17)
..write(obj.bangumiBadge)
..writeByte(18)
..write(obj.cardType)
..writeByte(19)
..write(obj.adInfo);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RecVideoItemAppModelAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class RcmdStatAdapter extends TypeAdapter<RcmdStat> {
@override
final int typeId = 1;
@override
RcmdStat read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RcmdStat(
view: fields[0] as String?,
like: fields[1] as String?,
danmu: fields[2] as String?,
);
}
@override
void write(BinaryWriter writer, RcmdStat obj) {
writer
..writeByte(3)
..writeByte(0)
..write(obj.view)
..writeByte(1)
..write(obj.like)
..writeByte(2)
..write(obj.danmu);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RcmdStatAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class RcmdOwnerAdapter extends TypeAdapter<RcmdOwner> {
@override
final int typeId = 2;
@override
RcmdOwner read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RcmdOwner(
name: fields[0] as String?,
mid: fields[1] as int?,
);
}
@override
void write(BinaryWriter writer, RcmdOwner obj) {
writer
..writeByte(2)
..writeByte(0)
..write(obj.name)
..writeByte(1)
..write(obj.mid);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RcmdOwnerAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}
class RcmdReasonAdapter extends TypeAdapter<RcmdReason> {
@override
final int typeId = 8;
@override
RcmdReason read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return RcmdReason(
content: fields[0] as String?,
);
}
@override
void write(BinaryWriter writer, RcmdReason obj) {
writer
..writeByte(1)
..writeByte(0)
..write(obj.content);
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RcmdReasonAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -224,10 +224,12 @@ class SearchLiveItemModel {
this.liveTime,
this.uname,
this.uface,
this.face,
this.userCover,
this.type,
this.title,
this.cover,
this.pic,
this.online,
this.rankIndex,
this.rankScore,
@@ -242,16 +244,19 @@ class SearchLiveItemModel {
String? liveTime;
String? uname;
String? uface;
String? face;
String? userCover;
String? type;
List? title;
String? cover;
String? pic;
int? online;
int? rankIndex;
int? rankScore;
int? roomid;
int? attentions;
String? cateName;
Map? watchedShow;
SearchLiveItemModel.fromJson(Map<String, dynamic> json) {
rankOffset = json['rank_offset'];
@@ -260,10 +265,12 @@ class SearchLiveItemModel {
liveTime = json['live_time'];
uname = json['uname'];
uface = json['uface'];
face = json['uface'];
userCover = json['user_cover'];
type = json['type'];
title = Em.regTitle(json['title']);
cover = json['cover'];
pic = json['cover'];
online = json['online'];
rankIndex = json['rank_index'];
rankScore = json['rank_score'];

View File

@@ -0,0 +1,37 @@
class BlackListDataModel {
BlackListDataModel({
this.list,
this.total,
});
List<BlackListItem>? list;
int? total;
BlackListDataModel.fromJson(Map<String, dynamic> json) {
list = json['list']
.map<BlackListItem>((e) => BlackListItem.fromJson(e))
.toList();
total = json['total'];
}
}
class BlackListItem {
BlackListItem({
this.face,
this.mid,
this.mtime,
this.uname,
});
String? face;
int? mid;
int? mtime;
String? uname;
BlackListItem.fromJson(Map<String, dynamic> json) {
face = json['face'];
mid = json['mid'];
mtime = json['mtime'];
uname = json['uname'];
}
}

View File

@@ -1,3 +1,5 @@
// ignore_for_file: constant_identifier_names
enum VideoQuality {
speed240,
flunt360,
@@ -89,3 +91,46 @@ extension AudioQualityDesc on AudioQuality {
];
get description => _descList[index];
}
enum VideoDecodeFormats {
AV1,
HEVC,
AVC,
}
extension VideoDecodeFormatsDesc on VideoDecodeFormats {
static final List<String> _descList = [
'AV1',
'HEVC',
'AVC',
];
get description => _descList[index];
}
extension VideoDecodeFormatsCode on VideoDecodeFormats {
static final List<String> _codeList = [
'av01',
'hev1',
'avc1',
];
get code => _codeList[index];
static VideoDecodeFormats? fromCode(String code) {
final index = _codeList.indexOf(code);
if (index != -1) {
return VideoDecodeFormats.values[index];
}
return null;
}
static VideoDecodeFormats? fromString(String val) {
var result = VideoDecodeFormats.values.first;
for (var i in _codeList) {
if (val.startsWith(i)) {
result = VideoDecodeFormats.values[_codeList.indexOf(i)];
break;
}
}
return result;
}
}

View File

@@ -29,7 +29,7 @@ class PlayUrlModel {
int? timeLength;
String? acceptFormat;
List<dynamic>? acceptDesc;
List<dynamic>? acceptQuality;
List<int>? acceptQuality;
int? videoCodecid;
String? seekParam;
String? seekType;
@@ -48,7 +48,7 @@ class PlayUrlModel {
timeLength = json['timelength'];
acceptFormat = json['accept_format'];
acceptDesc = json['accept_description'];
acceptQuality = json['accept_quality'];
acceptQuality = json['accept_quality'].map<int>((e) => e as int).toList();
videoCodecid = json['video_codecid'];
seekParam = json['seek_param'];
seekType = json['seek_type'];

View File

@@ -580,9 +580,11 @@ class UgcSeason {
intro = json['intro'];
signState = json['sign_state'];
attribute = json['attribute'];
sections = json['sections']
.map<SectionItem>((e) => SectionItem.fromJson(e))
.toList();
sections = json['sections'] != null
? json['sections']
.map<SectionItem>((e) => SectionItem.fromJson(e))
.toList()
: [];
stat = Stat.fromJson(json['stat']);
epCount = json['ep_count'];
seasonType = json['season_type'];

246
lib/pages/about/index.dart Normal file
View File

@@ -0,0 +1,246 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/github/latest.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:url_launcher/url_launcher.dart';
class AboutPage extends StatefulWidget {
const AboutPage({super.key});
@override
State<AboutPage> createState() => _AboutPageState();
}
class _AboutPageState extends State<AboutPage> {
final AboutController _aboutController = Get.put(AboutController());
@override
Widget build(BuildContext context) {
Color outline = Theme.of(context).colorScheme.outline;
TextStyle subTitleStyle =
TextStyle(fontSize: 13, color: Theme.of(context).colorScheme.outline);
return Scaffold(
appBar: AppBar(
title: Text('关于', style: Theme.of(context).textTheme.titleMedium),
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Divider(
thickness: 8,
height: 10,
color: Theme.of(context).colorScheme.onInverseSurface,
),
Image.asset(
'assets/images/logo/logo_android_2.png',
width: 150,
),
Text(
'PiliPala',
style: Theme.of(context).textTheme.titleMedium,
),
const SizedBox(height: 20),
Obx(
() => ListTile(
title: const Text("当前版本"),
trailing: Text(_aboutController.currentVersion.value,
style: subTitleStyle),
),
),
Obx(
() => ListTile(
onTap: () => _aboutController.onUpdate(),
title: const Text('最新版本'),
trailing: Text(
_aboutController.isLoading.value
? '正在获取'
: _aboutController.isUpdate.value
? '有新版本 ❤️${_aboutController.remoteVersion.value}'
: '当前已是最新版',
style: subTitleStyle,
),
),
),
// ListTile(
// onTap: () {},
// title: const Text('更新日志'),
// trailing: const Icon(
// Icons.arrow_forward_ios,
// size: 16,
// ),
// ),
Divider(
thickness: 8,
height: 30,
color: Theme.of(context).colorScheme.onInverseSurface,
),
ListTile(
onTap: () {},
title: const Text('作者'),
trailing: Text('guozhigq', style: subTitleStyle),
),
ListTile(
onTap: () {},
title: const Text('酷安'),
trailing: Text('影若风', style: subTitleStyle),
),
ListTile(
onTap: () => _aboutController.githubUrl(),
title: const Text('Github'),
trailing: Text(
'github.com/guozhigq/pilipala',
style: subTitleStyle,
),
),
ListTile(
onTap: () => _aboutController.feedback(),
title: const Text('问题反馈'),
trailing: Icon(
Icons.arrow_forward_ios,
size: 16,
color: outline,
),
),
ListTile(
onTap: () => _aboutController.qqChanel(),
title: const Text('QQ频道'),
trailing: Icon(
Icons.arrow_forward_ios,
size: 16,
color: outline,
),
),
ListTile(
onTap: () => _aboutController.tgChanel(),
title: const Text('TG频道'),
trailing: Icon(Icons.arrow_forward_ios, size: 16, color: outline),
),
Divider(
thickness: 8,
height: 30,
color: Theme.of(context).colorScheme.onInverseSurface,
),
],
),
),
);
}
}
class AboutController extends GetxController {
RxString currentVersion = ''.obs;
RxString remoteVersion = ''.obs;
late LatestDataModel remoteAppInfo;
RxBool isUpdate = true.obs;
RxBool isLoading = true.obs;
@override
void onInit() {
super.onInit();
init();
// 获取当前版本
getCurrentApp();
// 获取最新的版本
getRemoteApp();
}
// 获取设备信息
Future init() async {
DeviceInfoPlugin deviceInfo = DeviceInfoPlugin();
if (Platform.isAndroid) {
AndroidDeviceInfo androidInfo = await deviceInfo.androidInfo;
print(androidInfo.supportedAbis);
} else if (Platform.isIOS) {
IosDeviceInfo iosInfo = await deviceInfo.iosInfo;
print(iosInfo);
}
}
// 获取啊当前版本
Future getCurrentApp() async {
var result = await PackageInfo.fromPlatform();
currentVersion.value = result.version;
}
// 获取远程版本
Future getRemoteApp() async {
var result = await Request().get(Api.latestApp);
LatestDataModel data = LatestDataModel.fromJson(result.data);
remoteAppInfo = data;
remoteVersion.value = data.tagName!;
isUpdate.value =
Utils.needUpdate(currentVersion.value, remoteVersion.value);
isLoading.value = false;
}
// 跳转下载/本地更新
Future onUpdate() async {
// final dir = await getApplicationSupportDirectory();
// final path = '${dir.path}/pilipala.apk';
// var result = await Request()
// .downloadFile(remoteAppInfo.assets!.first.downloadUrl, path);
// print(result);
launchUrl(
Uri.parse('https://github.com/guozhigq/pilipala/releases'),
mode: LaunchMode.externalApplication,
);
}
// 跳转github
githubUrl() {
launchUrl(
Uri.parse('https://github.com/guozhigq/pilipala'),
mode: LaunchMode.externalApplication,
);
}
// 问题反馈
feedback() {
launchUrl(
Uri.parse('https://github.com/guozhigq/pilipala/issues'),
// 系统自带浏览器打开
mode: LaunchMode.externalApplication,
);
}
// qq频道
qqChanel() {
Clipboard.setData(
const ClipboardData(text: 'https://pd.qq.com/s/css9rdwga'),
);
SmartDialog.showToast(
'已复制,即将在浏览器打开',
displayTime: const Duration(milliseconds: 500),
).then(
(value) => launchUrl(
Uri.parse('https://pd.qq.com/s/css9rdwga'),
mode: LaunchMode.externalApplication,
),
);
}
// tg频道
tgChanel() {
Clipboard.setData(
const ClipboardData(text: 'https://t.me/+lm_oOVmF0RJiODk1'),
);
SmartDialog.showToast(
'已复制,即将在浏览器打开',
displayTime: const Duration(milliseconds: 500),
).then(
(value) => launchUrl(
Uri.parse('https://t.me/+lm_oOVmF0RJiODk1'),
mode: LaunchMode.externalApplication,
),
);
}
}

View File

@@ -1,3 +1,68 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/bangumi.dart';
import 'package:pilipala/models/bangumi/list.dart';
import 'package:pilipala/utils/storage.dart';
class BangumiController extends GetxController {}
class BangumiController extends GetxController {
final ScrollController scrollController = ScrollController();
RxList<BangumiListItemModel> bangumiList = [BangumiListItemModel()].obs;
RxList<BangumiListItemModel> bangumiFollowList = [BangumiListItemModel()].obs;
int _currentPage = 1;
bool isLoadingMore = true;
Box user = GStrorage.user;
RxBool userLogin = false.obs;
late int mid;
@override
void onInit() {
super.onInit();
if (user.get(UserBoxKey.userMid) != null) {
mid = int.parse(user.get(UserBoxKey.userMid).toString());
}
userLogin.value = user.get(UserBoxKey.userLogin) != null;
}
Future queryBangumiListFeed({type = 'init'}) async {
if (type == 'init') {
_currentPage = 1;
}
var result = await BangumiHttp.bangumiList(page: _currentPage);
if (result['status']) {
if (type == 'init') {
bangumiList.value = result['data'].list;
} else {
bangumiList.addAll(result['data'].list);
}
_currentPage += 1;
} else {}
isLoadingMore = false;
return result;
}
// 上拉加载
Future onLoad() async {
queryBangumiListFeed(type: 'onLoad');
}
// 我的订阅
Future queryBangumiFollow() async {
var result = await BangumiHttp.bangumiFollow(mid: 17340771);
if (result['status']) {
bangumiFollowList.value = result['data'].list;
} else {}
return result;
}
// 返回顶部并刷新
void animateToTop() async {
if (scrollController.offset >=
MediaQuery.of(Get.context!).size.height * 5) {
scrollController.jumpTo(0);
} else {
await scrollController.animateTo(0,
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
}
}
}

View File

@@ -7,7 +7,6 @@ import 'package:pilipala/http/search.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart';
import 'package:pilipala/utils/feed_back.dart';
@@ -36,7 +35,6 @@ class BangumiIntroController extends GetxController {
RxBool isLoading = false.obs;
// 视频详情 请求返回
Rx<VideoDetailData> videoDetail = VideoDetailData().obs;
Rx<BangumiInfoModel> bangumiDetail = BangumiInfoModel().obs;
// 请求返回的信息
@@ -89,11 +87,6 @@ class BangumiIntroController extends GetxController {
// 获取番剧简介&选集
Future queryBangumiIntro() async {
var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId);
if (result['status']) {
bangumiDetail.value = result['data'];
epId = bangumiDetail.value.episodes!.first.id;
}
if (userLogin) {
// 获取点赞状态
queryHasLikeVideo();
@@ -102,6 +95,11 @@ class BangumiIntroController extends GetxController {
// 获取收藏状态
queryHasFavVideo();
}
var result = await SearchHttp.bangumiInfo(seasonId: seasonId, epId: epId);
if (result['status']) {
bangumiDetail.value = result['data'];
epId = bangumiDetail.value.episodes!.first.id;
}
return result;
}
@@ -132,15 +130,10 @@ class BangumiIntroController extends GetxController {
Future actionLikeVideo() async {
var result = await VideoHttp.likeVideo(bvid: bvid, type: !hasLike.value);
if (result['status']) {
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;
}
SmartDialog.showToast(!hasLike.value ? '点赞成功 👍' : '取消赞');
hasLike.value = !hasLike.value;
bangumiDetail.value.stat!['likes'] =
bangumiDetail.value.stat!['likes'] + (!hasLike.value ? 1 : -1);
hasLike.refresh();
} else {
SmartDialog.showToast(result['msg']);
@@ -193,8 +186,8 @@ class BangumiIntroController extends GetxController {
if (res['status']) {
SmartDialog.showToast('投币成功 👏');
hasCoin.value = true;
videoDetail.value.stat!.coin =
videoDetail.value.stat!.coin! + _tempThemeValue;
bangumiDetail.value.stat!['coins'] =
bangumiDetail.value.stat!['coins'] + _tempThemeValue;
} else {
SmartDialog.showToast(res['msg']);
}
@@ -287,4 +280,13 @@ class BangumiIntroController extends GetxController {
await VideoHttp.bangumiDel(seasonId: bangumiDetail.value.seasonId);
SmartDialog.showToast(result['msg']);
}
Future queryVideoInFolder() async {
var result = await VideoHttp.videoInFolder(
mid: user.get(UserBoxKey.userMid), rid: IdUtils.bv2av(bvid));
if (result['status']) {
favFolderData.value = result['data'];
}
return result;
}
}

View File

@@ -22,7 +22,11 @@ import 'controller.dart';
import 'widgets/intro_detail.dart';
class BangumiIntroPanel extends StatefulWidget {
const BangumiIntroPanel({super.key});
final int? cid;
const BangumiIntroPanel({
Key? key,
this.cid,
}) : super(key: key);
@override
State<BangumiIntroPanel> createState() => _BangumiIntroPanelState();
@@ -33,6 +37,7 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
final BangumiIntroController bangumiIntroController =
Get.put(BangumiIntroController(), tag: Get.arguments['heroTag']);
BangumiInfoModel? bangumiDetail;
late Future _futureBuilderFuture;
// 添加页面缓存
@override
@@ -44,13 +49,14 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
bangumiIntroController.bangumiDetail.listen((value) {
bangumiDetail = value;
});
_futureBuilderFuture = bangumiIntroController.queryBangumiIntro();
}
@override
Widget build(BuildContext context) {
super.build(context);
return FutureBuilder(
future: bangumiIntroController.queryBangumiIntro(),
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data['status']) {
@@ -67,7 +73,11 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
);
}
} else {
return BangumiInfo(loadingStatus: true, bangumiDetail: bangumiDetail);
return BangumiInfo(
loadingStatus: true,
bangumiDetail: bangumiDetail,
cid: widget.cid,
);
}
},
);
@@ -77,11 +87,13 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
class BangumiInfo extends StatefulWidget {
final bool loadingStatus;
final BangumiInfoModel? bangumiDetail;
final int? cid;
const BangumiInfo({
Key? key,
this.loadingStatus = false,
this.bangumiDetail,
this.cid,
}) : super(key: key);
@override
@@ -89,21 +101,22 @@ class BangumiInfo extends StatefulWidget {
}
class _BangumiInfoState extends State<BangumiInfo> {
late BangumiInfoModel? bangumiItem;
final BangumiIntroController bangumiIntroController =
Get.put(BangumiIntroController(), tag: Get.arguments['heroTag']);
late VideoDetailController? videoDetailCtr;
String heroTag = Get.arguments['heroTag'];
late final BangumiIntroController bangumiIntroController;
late final VideoDetailController videoDetailCtr;
Box localCache = GStrorage.localCache;
late final BangumiInfoModel? bangumiItem;
late double sheetHeight;
int? cid;
@override
void initState() {
super.initState();
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
bangumiItem = bangumiIntroController.bangumiItem;
videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
sheetHeight = localCache.get('sheetHeight');
cid = widget.cid!;
}
// 收藏
@@ -160,13 +173,14 @@ class _BangumiInfoState extends State<BangumiInfo> {
),
if (bangumiItem != null &&
bangumiItem!.rating != null)
pBadge(
'评分 ${!widget.loadingStatus ? widget.bangumiDetail!.rating!['score']! : bangumiItem!.rating!['score']!}',
context,
null,
6,
6,
null),
PBadge(
text:
'评分 ${!widget.loadingStatus ? widget.bangumiDetail!.rating!['score']! : bangumiItem!.rating!['score']!}',
top: null,
right: 6,
bottom: 6,
left: null,
),
],
),
const SizedBox(width: 10),
@@ -318,9 +332,10 @@ class _BangumiInfoState extends State<BangumiInfo> {
pages: bangumiItem != null
? bangumiItem!.episodes!
: widget.bangumiDetail!.episodes!,
cid: bangumiItem != null
? bangumiItem!.episodes!.first.cid
: widget.bangumiDetail!.episodes!.first.cid,
cid: cid ??
(bangumiItem != null
? bangumiItem!.episodes!.first.cid
: widget.bangumiDetail!.episodes!.first.cid),
sheetHeight: sheetHeight,
changeFuc: (bvid, cid, aid) => bangumiIntroController
.changeSeasonOrbangu(bvid, cid, aid),
@@ -357,10 +372,10 @@ class _BangumiInfoState extends State<BangumiInfo> {
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: () => bangumiIntroController.actionLikeVideo(),
selectStatus: bangumiIntroController.hasLike.value,
loadingStatus: widget.loadingStatus,
loadingStatus: false,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['likes']!.toString()
: '-'),
: bangumiItem!.stat!['likes']!.toString()),
),
Obx(
() => ActionItem(
@@ -368,10 +383,10 @@ class _BangumiInfoState extends State<BangumiInfo> {
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: () => bangumiIntroController.actionCoinVideo(),
selectStatus: bangumiIntroController.hasCoin.value,
loadingStatus: widget.loadingStatus,
loadingStatus: false,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['coins']!.toString()
: '-'),
: bangumiItem!.stat!['coins']!.toString()),
),
Obx(
() => ActionItem(
@@ -379,29 +394,29 @@ class _BangumiInfoState extends State<BangumiInfo> {
selectIcon: const Icon(FontAwesomeIcons.solidStar),
onTap: () => showFavBottomSheet(),
selectStatus: bangumiIntroController.hasFav.value,
loadingStatus: widget.loadingStatus,
loadingStatus: false,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['favorite']!.toString()
: '-'),
: bangumiItem!.stat!['favorite']!.toString()),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.comment),
selectIcon: const Icon(FontAwesomeIcons.reply),
onTap: () => videoDetailCtr!.tabCtr!.animateTo(1),
onTap: () => videoDetailCtr.tabCtr.animateTo(1),
selectStatus: false,
loadingStatus: widget.loadingStatus,
loadingStatus: false,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['reply']!.toString()
: '-',
: bangumiItem!.stat!['reply']!.toString(),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare),
onTap: () => bangumiIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: widget.loadingStatus,
loadingStatus: false,
text: !widget.loadingStatus
? widget.bangumiDetail!.stat!['share']!.toString()
: '-'),
: bangumiItem!.stat!['share']!.toString()),
],
),
),
@@ -465,9 +480,6 @@ class _BangumiInfoState extends State<BangumiInfo> {
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: widget.loadingStatus,
// text: !widget.loadingStatus
// ? widget.videoDetail!.stat!.share!.toString()
// : '-',
text: '转发'),
]);
}

View File

@@ -1,4 +1,15 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/rcmd/view.dart';
import 'controller.dart';
import 'widgets/bangumu_card_v.dart';
class BangumiPage extends StatefulWidget {
const BangumiPage({super.key});
@@ -7,12 +18,181 @@ class BangumiPage extends StatefulWidget {
State<BangumiPage> createState() => _BangumiPageState();
}
class _BangumiPageState extends State<BangumiPage> {
class _BangumiPageState extends State<BangumiPage>
with AutomaticKeepAliveClientMixin {
final BangumiController _bangumidController = Get.put(BangumiController());
late Future? _futureBuilderFuture;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
ScrollController scrollController = _bangumidController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
_futureBuilderFuture = _bangumidController.queryBangumiListFeed();
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (!_bangumidController.isLoadingMore) {
_bangumidController.isLoadingMore = true;
await _bangumidController.onLoad();
}
}
final ScrollDirection direction =
scrollController.position.userScrollDirection;
if (direction == ScrollDirection.forward) {
mainStream.add(true);
} else if (direction == ScrollDirection.reverse) {
mainStream.add(false);
}
},
);
}
@override
Widget build(BuildContext context) {
return const Scaffold(
body: Center(
child: Text('还在开发中'),
super.build(context);
return RefreshIndicator(
onRefresh: () async {
await _bangumidController.queryBangumiListFeed(type: 'init');
return _bangumidController.queryBangumiFollow();
},
child: CustomScrollView(
controller: _bangumidController.scrollController,
slivers: [
SliverToBoxAdapter(
child: Obx(
() => Visibility(
visible: _bangumidController.userLogin.value,
child: Column(
children: [
Padding(
padding: const EdgeInsets.only(
top: StyleString.safeSpace, bottom: 10, left: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'最近追番',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
),
SizedBox(
height: 258,
child: FutureBuilder(
future: _bangumidController.queryBangumiFollow(),
builder: (context, snapshot) {
if (snapshot.connectionState ==
ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: _bangumidController
.bangumiFollowList.length,
itemBuilder: (context, index) {
return Container(
width: Get.size.width / 3,
height: 254,
margin: EdgeInsets.only(
left: StyleString.safeSpace,
right: index ==
_bangumidController
.bangumiFollowList
.length -
1
? StyleString.safeSpace
: 0),
child: BangumiCardV(
bangumiItem: _bangumidController
.bangumiFollowList[index],
),
);
},
),
);
} else {
return const SizedBox();
}
} else {
return const SizedBox();
}
},
),
),
],
),
),
),
),
SliverToBoxAdapter(
child: Padding(
padding: const EdgeInsets.only(top: 10, bottom: 10, left: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'推荐',
style: Theme.of(context).textTheme.titleMedium,
),
],
),
),
),
SliverPadding(
padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 0, StyleString.safeSpace, 0),
sliver: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(() => contentGrid(
_bangumidController, _bangumidController.bangumiList));
} else {
return HttpError(
errMsg: data['msg'],
fn: () => {},
);
}
} else {
return contentGrid(_bangumidController, []);
}
},
),
),
const LoadingMore()
],
),
);
}
Widget contentGrid(ctr, bangumiList) {
return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 行间距
mainAxisSpacing: StyleString.cardSpace - 2,
// 列间距
crossAxisSpacing: StyleString.cardSpace,
// 列数
crossAxisCount: 3,
mainAxisExtent: Get.size.width / 3 / 0.65 + 30,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return bangumiList!.isNotEmpty
? BangumiCardV(bangumiItem: bangumiList[index])
: const SizedBox();
},
childCount: bangumiList!.isNotEmpty ? bangumiList!.length : 10,
),
);
}

View File

@@ -22,73 +22,106 @@ class BangumiPanel extends StatefulWidget {
class _BangumiPanelState extends State<BangumiPanel> {
late int currentIndex;
final ScrollController listViewScrollCtr = ScrollController();
final ScrollController listViewScrollCtr_2 = ScrollController();
@override
void initState() {
super.initState();
currentIndex = widget.pages.indexWhere((e) => e.cid == widget.cid!);
scrollToIndex();
}
@override
void dispose() {
listViewScrollCtr.dispose();
listViewScrollCtr_2.dispose();
super.dispose();
}
void showBangumiPanel() {
showBottomSheet(
context: context,
builder: (_) => Container(
height: widget.sheetHeight,
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
Container(
height: 45,
padding: const EdgeInsets.only(left: 14, right: 14),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(const Duration(milliseconds: 200));
listViewScrollCtr_2.animateTo(currentIndex * 56,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut);
});
// 在这里使用 setState 更新状态
return Container(
height: widget.sheetHeight,
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
Text(
'合集(${widget.pages.length}',
style: Theme.of(context).textTheme.titleMedium,
AppBar(
toolbarHeight: 45,
automaticallyImplyLeading: false,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'合集(${widget.pages.length}',
style: Theme.of(context).textTheme.titleMedium,
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
titleSpacing: 10,
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
Expanded(
child: Material(
child: ListView.builder(
controller: listViewScrollCtr_2,
itemCount: widget.pages.length,
itemBuilder: (context, index) {
return ListTile(
onTap: () {
setState(() {
changeFucCall(widget.pages[index], index);
});
},
dense: false,
leading: index == currentIndex
? Image.asset(
'assets/images/live.gif',
color:
Theme.of(context).colorScheme.primary,
height: 12,
)
: null,
title: Text(
'${index + 1}${widget.pages[index].longTitle!}',
style: TextStyle(
fontSize: 14,
color: index == currentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
),
trailing: widget.pages[index].badge != null
? Image.asset(
'assets/images/big-vip.png',
height: 20,
)
: const SizedBox(),
);
},
),
),
),
],
),
),
Divider(
height: 1,
color: Theme.of(context).dividerColor.withOpacity(0.1),
),
Expanded(
child: Material(
child: ListView.builder(
itemCount: widget.pages.length,
itemBuilder: (context, index) {
return ListTile(
onTap: () => changeFucCall(widget.pages[index], index),
dense: false,
title: Text(
widget.pages[index].longTitle!,
style: TextStyle(
fontSize: 14,
color: index == currentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface,
),
),
trailing: widget.pages[index].badge != null
? Image.asset(
'assets/images/big-vip.png',
height: 20,
)
: const SizedBox(),
);
},
),
),
),
],
),
),
);
},
);
},
);
}
@@ -104,6 +137,15 @@ class _BangumiPanelState extends State<BangumiPanel> {
);
currentIndex = i;
setState(() {});
scrollToIndex();
}
void scrollToIndex() {
WidgetsBinding.instance.addPostFrameCallback((_) {
// 在回调函数中获取更新后的状态
listViewScrollCtr.animateTo(currentIndex * 150,
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
});
}
@override
@@ -146,8 +188,10 @@ class _BangumiPanelState extends State<BangumiPanel> {
SizedBox(
height: 60,
child: ListView.builder(
controller: listViewScrollCtr,
scrollDirection: Axis.horizontal,
itemCount: widget.pages.length,
itemExtent: 150,
itemBuilder: ((context, i) {
return Container(
width: 150,
@@ -217,87 +261,6 @@ class _BangumiPanelState extends State<BangumiPanel> {
);
})),
)
// SingleChildScrollView(
// padding: const EdgeInsets.only(top: 7, bottom: 7),
// scrollDirection: Axis.horizontal,
// child: ConstrainedBox(
// constraints: BoxConstraints(
// minWidth: MediaQuery.of(context).size.width,
// ),
// child: Row(
// children: [
// for (int i = 0; i < widget.pages.length; i++) ...[
// Container(
// width: 150,
// margin: const EdgeInsets.only(right: 10),
// child: Material(
// color: Theme.of(context).colorScheme.onInverseSurface,
// borderRadius: BorderRadius.circular(6),
// clipBehavior: Clip.hardEdge,
// child: InkWell(
// onTap: () => changeFucCall(widget.pages[i], i),
// child: Padding(
// padding: const EdgeInsets.symmetric(
// vertical: 8, horizontal: 10),
// child: Column(
// crossAxisAlignment: CrossAxisAlignment.start,
// children: [
// Row(
// children: [
// if (i == currentIndex) ...[
// Image.asset(
// 'assets/images/live.gif',
// color:
// Theme.of(context).colorScheme.primary,
// height: 12,
// ),
// const SizedBox(width: 6)
// ],
// Text(
// '第${i + 1}话',
// style: TextStyle(
// fontSize: 13,
// color: i == currentIndex
// ? Theme.of(context)
// .colorScheme
// .primary
// : Theme.of(context)
// .colorScheme
// .onSurface),
// ),
// const SizedBox(width: 2),
// if (widget.pages[i].badge != null) ...[
// Image.asset(
// 'assets/images/big-vip.png',
// height: 16,
// ),
// ],
// ],
// ),
// const SizedBox(height: 3),
// Text(
// widget.pages[i].longTitle!,
// maxLines: 1,
// style: TextStyle(
// fontSize: 13,
// color: i == currentIndex
// ? Theme.of(context).colorScheme.primary
// : Theme.of(context)
// .colorScheme
// .onSurface),
// overflow: TextOverflow.ellipsis,
// )
// ],
// ),
// ),
// ),
// ),
// ),
// ]
// ],
// ),
// ),
// )
],
);
}

View File

@@ -0,0 +1,184 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
// 视频卡片 - 垂直布局
class BangumiCardV extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables
final bangumiItem;
final Function()? longPress;
final Function()? longPressEnd;
const BangumiCardV({
Key? key,
required this.bangumiItem,
this.longPress,
this.longPressEnd,
}) : super(key: key);
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(bangumiItem.mediaId);
return Card(
elevation: 0,
clipBehavior: Clip.hardEdge,
shape: RoundedRectangleBorder(
borderRadius: StyleString.mdRadius,
),
margin: EdgeInsets.zero,
child: GestureDetector(
// onLongPress: () {
// if (longPress != null) {
// longPress!();
// }
// },
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async {
int seasonId = bangumiItem.seasonId;
SmartDialog.showLoading(msg: '获取中...');
var res = await SearchHttp.bangumiInfo(seasonId: seasonId);
SmartDialog.dismiss().then((value) {
if (res['status']) {
if (res['data'].episodes.isEmpty) {
SmartDialog.showToast('资源加载失败');
return;
}
EpisodeItem episode = res['data'].episodes.first;
String bvid = episode.bvid!;
int cid = episode.cid!;
String pic = episode.cover!;
String heroTag = Utils.makeHeroTag(cid);
Get.toNamed(
'/video?bvid=$bvid&cid=$cid&seasonId=$seasonId',
arguments: {
'pic': pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'],
},
);
}
});
},
child: Column(
children: [
ClipRRect(
borderRadius: const BorderRadius.only(
topLeft: StyleString.imgRadius,
topRight: StyleString.imgRadius,
bottomLeft: StyleString.imgRadius,
bottomRight: StyleString.imgRadius,
),
child: AspectRatio(
aspectRatio: 0.65,
child: LayoutBuilder(builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: bangumiItem.cover,
width: maxWidth,
height: maxHeight,
),
),
if (bangumiItem.badge != null)
PBadge(
text: bangumiItem.badge,
top: 6,
right: 6,
bottom: null,
left: null),
if (bangumiItem.order != null)
PBadge(
text: bangumiItem.order,
top: null,
right: null,
bottom: 6,
left: 6,
type: 'gray',
),
],
);
}),
),
),
BangumiContent(bangumiItem: bangumiItem)
],
),
),
),
);
}
}
class BangumiContent extends StatelessWidget {
// ignore: prefer_typing_uninitialized_variables
final bangumiItem;
const BangumiContent({Key? key, required this.bangumiItem}) : super(key: key);
@override
Widget build(BuildContext context) {
return Expanded(
child: Padding(
// 多列
padding: const EdgeInsets.fromLTRB(4, 5, 0, 3),
// 单列
// padding: const EdgeInsets.fromLTRB(14, 10, 4, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Expanded(
child: Text(
bangumiItem.title,
textAlign: TextAlign.start,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
letterSpacing: 0.3,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
)),
],
),
if (bangumiItem.indexShow != null)
Text(
bangumiItem.indexShow,
maxLines: 1,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
if (bangumiItem.progress != null)
Text(
bangumiItem.progress,
maxLines: 1,
style: TextStyle(
fontSize: Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline,
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,156 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/black.dart';
import 'package:pilipala/models/user/black.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
class BlackListPage extends StatefulWidget {
const BlackListPage({super.key});
@override
State<BlackListPage> createState() => _BlackListPageState();
}
class _BlackListPageState extends State<BlackListPage> {
final BlackListController _blackListController =
Get.put(BlackListController());
final ScrollController scrollController = ScrollController();
Future? _futureBuilderFuture;
bool _isLoadingMore = false;
Box setting = GStrorage.setting;
@override
void initState() {
super.initState();
_futureBuilderFuture = _blackListController.queryBlacklist();
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
if (!_isLoadingMore) {
_isLoadingMore = true;
await _blackListController.queryBlacklist(type: 'onLoad');
_isLoadingMore = false;
}
}
},
);
}
@override
void dispose() {
List<int> blackMidsList =
_blackListController.blackList.map<int>((e) => e.mid!).toList();
setting.put(SettingBoxKey.blackMidsList, blackMidsList);
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0,
scrolledUnderElevation: 0,
titleSpacing: 0,
centerTitle: false,
title: Obx(
() => Text(
'黑名单管理 ${_blackListController.blackList.length} / 5000',
style: Theme.of(context).textTheme.titleMedium,
),
),
),
body: RefreshIndicator(
onRefresh: () async => await _blackListController.queryBlacklist(),
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
var data = snapshot.data;
if (data['status']) {
List<BlackListItem> list = _blackListController.blackList;
return Obx(
() => list.length == 1
? const SizedBox()
: ListView.builder(
controller: scrollController,
itemCount: list.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
onTap: () {},
leading: NetworkImgLayer(
width: 45,
height: 45,
type: 'avatar',
src: list[index].face,
),
title: Text(
list[index].uname!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 14),
),
subtitle: Text(
Utils.dateFormat(list[index].mtime),
maxLines: 1,
style: TextStyle(
color:
Theme.of(context).colorScheme.outline),
overflow: TextOverflow.ellipsis,
),
dense: true,
// trailing: TextButton(
// onPressed: () {},
// child: const Text('移除'),
// ),
);
},
),
);
} else {
return CustomScrollView(
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => _blackListController.queryBlacklist(),
)
],
);
}
} else {
// 骨架屏
return const SizedBox();
}
},
),
),
);
}
}
class BlackListController extends GetxController {
int currentPage = 1;
int pageSize = 50;
RxList<BlackListItem> blackList = [BlackListItem()].obs;
Future queryBlacklist({type = 'init'}) async {
if (type == 'init') {
currentPage = 1;
}
var result = await BlackHttp.blackList(pn: currentPage, ps: pageSize);
if (result['status']) {
if (type == 'init') {
blackList.value = result['data'].list;
} else {
blackList.addAll(result['data'].list);
}
currentPage += 1;
}
return result;
}
}

View File

@@ -3,13 +3,18 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/dynamics.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/dynamics_type.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
class DynamicsController extends GetxController {
int page = 1;
@@ -46,8 +51,19 @@ class DynamicsController extends GetxController {
];
bool flag = false;
RxInt initialValue = 1.obs;
Box user = GStrorage.user;
RxBool userLogin = false.obs;
@override
void onInit() {
userLogin.value = user.get(UserBoxKey.userLogin, defaultValue: false);
super.onInit();
}
Future queryFollowDynamic({type = 'init'}) async {
if (!userLogin.value) {
return {'status': false, 'msg': '未登录'};
}
if (type == 'init') {
dynamicsList.clear();
}
@@ -142,10 +158,39 @@ class DynamicsController extends GetxController {
/// TODO
case 'DYNAMIC_TYPE_UGC_SEASON':
print('合集');
break;
case 'DYNAMIC_TYPE_PGC_UNION':
print('DYNAMIC_TYPE_PGC_UNION 番剧');
DynamicArchiveModel pgc = item.modules.moduleDynamic.major.pgc;
if (pgc.epid != null) {
SmartDialog.showLoading(msg: '获取中...');
var res = await SearchHttp.bangumiInfo(epId: pgc.epid);
SmartDialog.dismiss();
if (res['status']) {
EpisodeItem episode = res['data'].episodes.first;
String bvid = episode.bvid!;
int cid = episode.cid!;
String pic = episode.cover!;
String heroTag = Utils.makeHeroTag(cid);
Get.toNamed(
'/video?bvid=$bvid&cid=$cid&seasonId=${res['data'].seasonId}',
arguments: {
'pic': pic,
'heroTag': heroTag,
'videoType': SearchType.media_bangumi,
'bangumiItem': res['data'],
},
);
}
}
break;
}
}
Future queryFollowUp() async {
Future queryFollowUp({type = 'init'}) async {
if (type == 'init') {
upData = FollowUpModel().obs;
}
var res = await DynamicsHttp.followUp();
if (res['status']) {
upData.value = res['data'];

View File

@@ -1,17 +1,15 @@
import 'dart:async';
import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/skeleton/dynamic_card.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
@@ -29,9 +27,11 @@ class DynamicsPage extends StatefulWidget {
class _DynamicsPageState extends State<DynamicsPage>
with AutomaticKeepAliveClientMixin {
final DynamicsController _dynamicsController = Get.put(DynamicsController());
Future? _futureBuilderFuture;
late Future _futureBuilderFuture;
late Future _futureBuilderFutureUp;
bool _isLoadingMore = false;
Box user = GStrorage.user;
EventBus eventBus = EventBus();
@override
bool get wantKeepAlive => true;
@@ -40,6 +40,7 @@ class _DynamicsPageState extends State<DynamicsPage>
void initState() {
super.initState();
_futureBuilderFuture = _dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp = _dynamicsController.queryFollowUp();
ScrollController scrollController = _dynamicsController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
@@ -63,6 +64,14 @@ class _DynamicsPageState extends State<DynamicsPage>
}
},
);
eventBus.on(EventName.loginEvent, (args) {
_dynamicsController.userLogin.value = args['status'];
setState(() {
_futureBuilderFuture = _dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp = _dynamicsController.queryFollowUp();
});
});
}
@override
@@ -108,115 +117,82 @@ class _DynamicsPageState extends State<DynamicsPage>
return const SizedBox();
}
}),
Obx(() => Visibility(
visible: _dynamicsController.mid.value == -1,
child: CustomSlidingSegmentedControl<int>(
initialValue: _dynamicsController.initialValue.value,
children: {
1: Text(
'全部',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
Obx(
() => _dynamicsController.userLogin.value
? Visibility(
visible: _dynamicsController.mid.value == -1,
child: CustomSlidingSegmentedControl<int>(
initialValue:
_dynamicsController.initialValue.value,
children: {
1: Text(
'全部',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize),
),
2: Text('投稿',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize)),
3: Text('番剧',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize)),
// 4: Text(
// '专栏',
// style: TextStyle(
// fontSize: Theme.of(context)
// .textTheme
// .labelMedium!
// .fontSize),
// ),
},
padding: 13.0,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceVariant
.withOpacity(0.7),
borderRadius: BorderRadius.circular(20),
),
thumbDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(20),
),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
onValueChanged: (v) {
feedBack();
_dynamicsController.onSelectType(v);
},
),
2: Text('投稿',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize)),
3: Text('番剧',
style: TextStyle(
fontSize: Theme.of(context)
.textTheme
.labelMedium!
.fontSize)),
// 4: Text(
// '专栏',
// style: TextStyle(
// fontSize: Theme.of(context)
// .textTheme
// .labelMedium!
// .fontSize),
// ),
},
padding: 13.0,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceVariant
.withOpacity(0.7),
borderRadius: BorderRadius.circular(20),
),
thumbDecoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: BorderRadius.circular(20),
),
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut,
onValueChanged: (v) {
feedBack();
_dynamicsController.onSelectType(v);
},
),
))
)
: Text('动态',
style: Theme.of(context).textTheme.titleMedium),
)
],
),
Positioned(
right: 4,
top: 0,
bottom: 0,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: () =>
{feedBack(), _dynamicsController.resetSearch()},
icon: const Icon(Icons.history, size: 21),
),
),
Positioned(
left: 10,
top: 0,
bottom: 0,
child: Align(
alignment: Alignment.center,
child: user.get(UserBoxKey.userLogin) ?? false
? GestureDetector(
onTap: () {
feedBack();
showModalBottomSheet(
context: context,
builder: (_) => const SizedBox(
height: 450,
child: MinePage(),
),
clipBehavior: Clip.hardEdge,
isScrollControlled: true,
);
},
child: NetworkImgLayer(
type: 'avatar',
width: 30,
height: 30,
src: user.get(UserBoxKey.userFace),
),
)
: IconButton(
onPressed: () {
feedBack();
showModalBottomSheet(
context: context,
builder: (_) => const SizedBox(
height: 450,
child: MinePage(),
),
clipBehavior: Clip.hardEdge,
isScrollControlled: true,
);
},
icon: const Icon(CupertinoIcons.person, size: 22),
),
Obx(
() => Visibility(
visible: _dynamicsController.userLogin.value,
child: Positioned(
right: 4,
top: 0,
bottom: 0,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: () =>
{feedBack(), _dynamicsController.resetSearch()},
icon: const Icon(Icons.history, size: 21),
),
),
),
),
],
@@ -229,7 +205,7 @@ class _DynamicsPageState extends State<DynamicsPage>
controller: _dynamicsController.scrollController,
slivers: [
FutureBuilder(
future: _dynamicsController.queryFollowUp(),
future: _futureBuilderFutureUp,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data;
@@ -269,7 +245,14 @@ class _DynamicsPageState extends State<DynamicsPage>
} else {
return HttpError(
errMsg: data['msg'],
fn: () => _dynamicsController.onRefresh(),
fn: () {
setState(() {
_futureBuilderFuture =
_dynamicsController.queryFollowDynamic();
_futureBuilderFutureUp =
_dynamicsController.queryFollowUp();
});
},
);
}
} else {

View File

@@ -36,7 +36,8 @@ Widget author(item, context) {
Text(
item.modules.moduleAuthor.name,
style: TextStyle(
color: item.modules.moduleAuthor!.vip!['status'] > 0
color: item.modules.moduleAuthor!.vip != null &&
item.modules.moduleAuthor!.vip['status'] > 0
? const Color.fromARGB(255, 251, 100, 163)
: Theme.of(context).colorScheme.onBackground,
fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,

View File

@@ -77,10 +77,21 @@ Widget liveRcmdPanel(item, context, {floor = 1}) {
src: item.modules.moduleDynamic.major.liveRcmd.cover,
),
),
pBadge(watchedShow['text_large'], context, 6, 56, null, null,
type: 'gray'),
pBadge(
liveStatus == 1 ? '直播中' : '直播结束', context, 6, 6, null, null),
PBadge(
text: watchedShow['text_large'],
top: 6,
right: 56,
bottom: null,
left: null,
type: 'gray',
),
PBadge(
text: liveStatus == 1 ? '直播中' : '直播结束',
top: 6,
right: 6,
bottom: null,
left: null,
),
Positioned(
left: 0,
right: 0,

View File

@@ -85,7 +85,14 @@ Widget picWidget(item, context) {
children: list,
),
if (len == 1 && origAspectRatio < 0.4)
pBadge('长图', context, null, null, 6.0, 6.0, type: 'gray')
const PBadge(
text: '长图',
top: null,
right: null,
bottom: 6.0,
left: 6.0,
type: 'gray',
)
],
),
);

View File

@@ -40,7 +40,7 @@ class _UpPanelState extends State<UpPanel> {
1,
UpItem(
face: user.get(UserBoxKey.userFace),
uname: '',
uname: '',
mid: user.get(UserBoxKey.userMid),
),
);

View File

@@ -89,7 +89,13 @@ Widget videoSeasonWidget(item, context, type, {floor = 1}) {
),
),
if (content.badge != null && type == 'pgc')
pBadge(content.badge['text'], context, 8.0, 10.0, null, null),
PBadge(
text: content.badge['text'],
top: 8.0,
right: 10.0,
bottom: null,
left: null,
),
Positioned(
left: 0,
right: 0,

View File

@@ -13,6 +13,13 @@ class FavPage extends StatefulWidget {
class _FavPageState extends State<FavPage> {
final FavController _favController = Get.put(FavController());
late Future _futureBuilderFuture;
@override
void initState() {
super.initState();
_futureBuilderFuture = _favController.queryFavFolder();
}
@override
Widget build(BuildContext context) {
@@ -26,7 +33,7 @@ class _FavPageState extends State<FavPage> {
),
),
body: FutureBuilder(
future: _favController.queryFavFolder(),
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;

View File

@@ -70,6 +70,7 @@ class HistoryController extends GetxController {
SmartDialog.showToast(
!pauseStatus.value ? '暂停观看历史' : '恢复观看历史');
pauseStatus.value = !pauseStatus.value;
localCache.put(LocalCacheKey.historyPause, pauseStatus.value);
}
SmartDialog.dismiss();
},
@@ -85,7 +86,7 @@ class HistoryController extends GetxController {
Future historyStatus() async {
var res = await UserHttp.historyStatus();
pauseStatus.value = res.data['data'];
localCache.put(LocalCacheKey.historyStatus, res.data['data']);
localCache.put(LocalCacheKey.historyPause, res.data['data']);
}
// 清空观看历史

View File

@@ -5,6 +5,7 @@ import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/business_type.dart';
@@ -147,23 +148,26 @@ class HistoryItem extends StatelessWidget {
if (!BusinessType
.hiddenDurationType.hiddenDurationType
.contains(videoItem.history.business))
pBadge(
videoItem.progress == -1
? '已看完'
: '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
context,
null,
6.0,
6.0,
null,
type: 'gray'),
PBadge(
text: videoItem.progress == -1
? '已看完'
: '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
right: 6.0,
bottom: 6.0,
type: 'gray',
),
// 右上角
if (BusinessType.showBadge.showBadge
.contains(videoItem.history.business) ||
videoItem.history.business ==
BusinessType.live.type)
pBadge(videoItem.badge, context, 6.0, 6.0,
null, null),
PBadge(
text: videoItem.badge,
top: 6.0,
right: 6.0,
bottom: null,
left: null,
),
],
);
},
@@ -229,6 +233,7 @@ class VideoContent extends StatelessWidget {
],
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
Utils.dateFormat(videoItem.viewAt!),
@@ -236,7 +241,46 @@ class VideoContent extends StatelessWidget {
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline),
)
),
if (videoItem.badge != '番剧' &&
!videoItem.tagName.contains('动画') &&
videoItem.history.business != 'live' &&
!videoItem.history.business.contains('article'))
SizedBox(
width: 24,
height: 24,
child: PopupMenuButton<String>(
padding: EdgeInsets.zero,
tooltip: '稍后再看',
icon: Icon(
Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline,
size: 14,
),
position: PopupMenuPosition.under,
// constraints: const BoxConstraints(maxHeight: 35),
onSelected: (String type) {},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
PopupMenuItem<String>(
onTap: () async {
var res = await UserHttp.toViewLater(
bvid: videoItem.history.bvid);
SmartDialog.showToast(res['msg']);
},
value: 'pause',
height: 35,
child: const Row(
children: [
Icon(Icons.watch_later_outlined, size: 16),
SizedBox(width: 6),
Text('稍后再看', style: TextStyle(fontSize: 13))
],
),
),
],
),
),
],
),
],

View File

@@ -1,84 +1,54 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/pages/bangumi/index.dart';
import 'package:pilipala/pages/hot/index.dart';
import 'package:pilipala/pages/live/index.dart';
import 'package:pilipala/pages/rcmd/index.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/common/tab_type.dart';
import 'package:pilipala/utils/storage.dart';
class HomeController extends GetxController with GetTickerProviderStateMixin {
bool flag = false;
List tabs = [
{
'icon': const Icon(
Icons.live_tv_outlined,
size: 15,
),
'label': '直播',
'type': 'live'
},
{
'icon': const Icon(
Icons.thumb_up_off_alt_outlined,
size: 15,
),
'label': '推荐',
'type': 'rcm'
},
{
'icon': const Icon(
Icons.whatshot_outlined,
size: 15,
),
'label': '热门',
'type': 'hot'
},
{
'icon': const Icon(
Icons.play_circle_outlined,
size: 15,
),
'label': '番剧',
'type': 'bangumi'
},
];
late List tabs;
int initialIndex = 1;
late TabController tabController;
List ctrList = [
Get.find<LiveController>,
Get.find<RcmdController>,
Get.find<HotController>,
Get.find<BangumiController>,
];
RxString defaultSearch = '输入关键词搜索'.obs;
late List tabsCtrList;
late List<Widget> tabsPageList;
Box user = GStrorage.user;
RxBool userLogin = false.obs;
RxString userFace = ''.obs;
@override
void onInit() {
super.onInit();
userLogin.value = user.get(UserBoxKey.userLogin) ?? false;
userFace.value = user.get(UserBoxKey.userFace) ?? '';
// 进行tabs配置
tabs = tabsConfig;
tabsCtrList = tabsConfig.map((e) => e['ctr']).toList();
tabsPageList = tabsConfig.map<Widget>((e) => e['page']).toList();
tabController = TabController(
initialIndex: initialIndex,
length: tabs.length,
vsync: this,
);
searchDefault();
}
void onRefresh() {
int index = tabController.index;
var ctr = ctrList[index];
var ctr = tabsCtrList[index];
ctr().onRefresh();
}
void animateToTop() {
int index = tabController.index;
var ctr = ctrList[index];
var ctr = tabsCtrList[index];
ctr().animateToTop();
}
void searchDefault() async {
var res = await Request().get(Api.searchDefault);
if (res.data['code'] == 0) {
defaultSearch.value = res.data['data']['name'];
}
// 更新登录状态
void updateLoginStatus(val) {
userLogin.value = val ?? false;
userFace.value = user.get(UserBoxKey.userFace) ?? '';
}
}

View File

@@ -1,16 +1,10 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/pages/bangumi/index.dart';
import 'package:pilipala/pages/hot/index.dart';
import 'package:pilipala/pages/live/index.dart';
import 'package:pilipala/pages/main/index.dart';
import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/pages/rcmd/index.dart';
import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import './controller.dart';
class HomePage extends StatefulWidget {
@@ -29,6 +23,19 @@ class _HomePageState extends State<HomePage>
@override
bool get wantKeepAlive => true;
showUserBottonSheet() {
feedBack();
showModalBottomSheet(
context: context,
builder: (_) => const SizedBox(
height: 450,
child: MinePage(),
),
clipBehavior: Clip.hardEdge,
isScrollControlled: true,
);
}
@override
Widget build(BuildContext context) {
super.build(context);
@@ -38,73 +45,40 @@ class _HomePageState extends State<HomePage>
appBar: AppBar(toolbarHeight: 0, elevation: 0),
body: Column(
children: [
CustomAppBar(stream: stream, ctr: _homeController),
Container(
padding: const EdgeInsets.only(left: 12, right: 12, bottom: 4),
child: Stack(
children: [
Align(
alignment: Alignment.center,
child: Theme(
data: ThemeData(
splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明
highlightColor: Colors.transparent, // 点击时的背景高亮颜色设置为透明
),
child: Padding(
padding: const EdgeInsets.only(top: 2),
child: TabBar(
controller: _homeController.tabController,
tabs: [
for (var i in _homeController.tabs)
// Tab(text: i['label'])
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 0, vertical: 11),
child: Row(
children: [
i['icon'],
const SizedBox(width: 4),
Text(i['label'])
],
),
),
],
isScrollable: true,
indicatorWeight: 0,
indicatorPadding: const EdgeInsets.symmetric(
horizontal: 4, vertical: 5),
indicator: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.8),
borderRadius:
const BorderRadius.all(Radius.circular(20)),
),
indicatorSize: TabBarIndicatorSize.tab,
labelColor: Theme.of(context).colorScheme.primary,
labelStyle: const TextStyle(fontSize: 13),
dividerColor: Colors.transparent,
unselectedLabelColor:
Theme.of(context).colorScheme.outline,
onTap: (value) =>
{feedBack(), _homeController.initialIndex = value},
),
),
),
),
],
CustomAppBar(
stream: stream,
ctr: _homeController,
callback: showUserBottonSheet,
),
const SizedBox(height: 8),
SizedBox(
width: double.infinity,
height: 42,
child: Align(
alignment: Alignment.center,
child: TabBar(
controller: _homeController.tabController,
tabs: [
for (var i in _homeController.tabs) Tab(text: i['label'])
],
isScrollable: true,
dividerColor: Colors.transparent,
enableFeedback: true,
splashBorderRadius: BorderRadius.circular(10),
onTap: (value) {
feedBack();
if (_homeController.initialIndex == value) {
_homeController.tabsCtrList[value]().animateToTop();
}
_homeController.initialIndex = value;
},
),
),
),
Expanded(
child: TabBarView(
controller: _homeController.tabController,
children: const [
LivePage(),
RcmdPage(),
HotPage(),
BangumiPage(),
],
children: _homeController.tabsPageList,
),
),
],
@@ -116,13 +90,15 @@ class _HomePageState extends State<HomePage>
class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
final double height;
final Stream<bool>? stream;
final ctr;
final HomeController? ctr;
final Function? callback;
const CustomAppBar({
super.key,
this.height = kToolbarHeight,
this.stream,
this.ctr,
this.callback,
});
@override
@@ -130,125 +106,81 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget {
@override
Widget build(BuildContext context) {
Box user = GStrorage.user;
return StreamBuilder(
stream: stream,
initialData: true,
builder: (context, AsyncSnapshot snapshot) {
return ClipRect(
clipBehavior: Clip.hardEdge,
child: AnimatedOpacity(
opacity: snapshot.data ? 1 : 0,
duration: const Duration(milliseconds: 300),
child: AnimatedContainer(
curve: Curves.linear,
duration: const Duration(milliseconds: 300),
height: snapshot.data ? 94 : MediaQuery.of(context).padding.top,
child: Container(
padding: EdgeInsets.only(
left: 12,
right: 12,
bottom: 4,
top: MediaQuery.of(context).padding.top,
),
child: Row(
children: [
const Text(
'PLPL',
style: TextStyle(
height: 2.8,
fontSize: 17,
fontWeight: FontWeight.bold,
fontFamily: 'Jura-Bold',
),
),
const SizedBox(width: 10),
Expanded(
child: GestureDetector(
onTap: () {
Get.toNamed('/search', parameters: {
'hintText': ctr.defaultSearch.value
});
},
child: Container(
width: 250,
height: 45,
clipBehavior: Clip.hardEdge,
padding: const EdgeInsets.only(left: 12, right: 22),
decoration: BoxDecoration(
borderRadius:
const BorderRadius.all(Radius.circular(25)),
color:
Theme.of(context).colorScheme.onInverseSurface,
),
child: Row(
return AnimatedOpacity(
opacity: snapshot.data ? 1 : 0,
duration: const Duration(milliseconds: 300),
child: AnimatedContainer(
curve: Curves.easeInOutCubicEmphasized,
duration: const Duration(milliseconds: 500),
height: snapshot.data
? MediaQuery.of(context).padding.top + 52
: MediaQuery.of(context).padding.top,
child: Container(
padding: EdgeInsets.only(
left: 20,
right: 20,
bottom: 0,
top: MediaQuery.of(context).padding.top + 4,
),
child: Row(
children: [
const Expanded(child: SearchPage()),
const SizedBox(width: 10),
Obx(
() => ctr!.userLogin.value
? Stack(
children: [
Icon(
Icons.search_outlined,
size: 23,
color: Theme.of(context).colorScheme.outline,
NetworkImgLayer(
type: 'avatar',
width: 34,
height: 34,
src: ctr!.userFace.value,
),
const SizedBox(width: 7),
Expanded(
child: Obx(
() => Text(
ctr.defaultSearch.value,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline),
Positioned.fill(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () => callback!(),
splashColor: Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.3),
borderRadius: const BorderRadius.all(
Radius.circular(50),
),
),
),
),
)
],
)
: SizedBox(
width: 38,
height: 38,
child: IconButton(
style: ButtonStyle(
padding:
MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor:
MaterialStateProperty.resolveWith((states) {
return Theme.of(context)
.colorScheme
.onInverseSurface;
}),
),
onPressed: () => callback!(),
icon: Icon(
Icons.person_rounded,
size: 22,
color: Theme.of(context).colorScheme.primary,
),
),
),
),
),
),
const SizedBox(width: 12),
if (user.get(UserBoxKey.userLogin) ?? false) ...[
GestureDetector(
onTap: () {
feedBack();
showModalBottomSheet(
context: context,
builder: (_) => const SizedBox(
height: 450,
child: MinePage(),
),
clipBehavior: Clip.hardEdge,
isScrollControlled: true,
);
},
child: NetworkImgLayer(
type: 'avatar',
width: 34,
height: 34,
src: user.get(UserBoxKey.userFace),
),
)
] else ...[
IconButton(
onPressed: () {
feedBack();
showModalBottomSheet(
context: context,
builder: (_) => const SizedBox(
height: 450,
child: MinePage(),
),
clipBehavior: Clip.hardEdge,
isScrollControlled: true,
);
},
icon: const Icon(CupertinoIcons.person, size: 22),
)
],
],
),
),
],
),
),
),

View File

@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/animated_dialog.dart';
import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/skeleton/video_card_h.dart';
@@ -59,52 +60,57 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
super.build(context);
return Scaffold(
body: RefreshIndicator(
displacement: kToolbarHeight + MediaQuery.of(context).padding.top,
onRefresh: () async {
return await _hotController.onRefresh();
},
child: CustomScrollView(
controller: _hotController.scrollController,
slivers: [
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return VideoCardH(
videoItem: _hotController.videoList[index],
longPress: () {
_hotController.popupDialog = _createPopupDialog(
_hotController.videoList[index]);
Overlay.of(context)
.insert(_hotController.popupDialog!);
},
longPressEnd: () {
_hotController.popupDialog?.remove();
},
);
}, childCount: _hotController.videoList.length),
),
);
SliverPadding(
// 单列布局 EdgeInsets.zero
padding:
const EdgeInsets.fromLTRB(0, StyleString.safeSpace - 5, 0, 0),
sliver: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => SliverList(
delegate:
SliverChildBuilderDelegate((context, index) {
return VideoCardH(
videoItem: _hotController.videoList[index],
longPress: () {
_hotController.popupDialog = _createPopupDialog(
_hotController.videoList[index]);
Overlay.of(context)
.insert(_hotController.popupDialog!);
},
longPressEnd: () {
_hotController.popupDialog?.remove();
},
);
}, childCount: _hotController.videoList.length),
),
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
);
}
} else {
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
}, childCount: 10),
);
}
} else {
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
}, childCount: 10),
);
}
},
},
),
),
SliverToBoxAdapter(
child: SizedBox(
@@ -120,7 +126,9 @@ class _HotPageState extends State<HotPage> with AutomaticKeepAliveClientMixin {
OverlayEntry _createPopupDialog(videoItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
child: OverlayPop(videoItem: videoItem),
closeFn: _hotController.popupDialog?.remove,
child: OverlayPop(
videoItem: videoItem, closeFn: _hotController.popupDialog?.remove),
),
);
}

View File

@@ -6,15 +6,20 @@ import 'package:pilipala/models/model_hot_video_item.dart';
class LaterController extends GetxController {
final ScrollController scrollController = ScrollController();
RxList<HotVideoItemModel> laterList = [HotVideoItemModel()].obs;
RxList<HotVideoItemModel> laterList = <HotVideoItemModel>[].obs;
int count = 0;
RxBool isLoading = false.obs;
Future queryLaterList() async {
isLoading.value = true;
var res = await UserHttp.seeYouLater();
if (res['status']) {
laterList.value = res['data']['list'];
count = res['data']['count'];
if (count > 0) {
laterList.value = res['data']['list'];
}
}
isLoading.value = false;
return res;
}
@@ -47,4 +52,34 @@ class LaterController extends GetxController {
},
);
}
// 一键清空
Future toViewClear() async {
SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('清空确认'),
content: const Text('确定要清空你的稍后再看列表吗?'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
child: const Text('取消')),
TextButton(
onPressed: () async {
var res = await UserHttp.toViewClear();
if (res['status']) {
laterList.clear();
}
SmartDialog.dismiss();
SmartDialog.showToast(res['msg']);
},
child: const Text('确认'),
)
],
);
},
);
}
}

View File

@@ -29,25 +29,38 @@ class _LaterPageState extends State<LaterPage> {
titleSpacing: 0,
centerTitle: false,
title: Obx(
() => Text(
'稍后再看 (${_laterController.laterList.length}/100)',
style: Theme.of(context).textTheme.titleMedium,
),
() => _laterController.laterList.isNotEmpty
? Text(
'稍后再看 (${_laterController.laterList.length}/100)',
style: Theme.of(context).textTheme.titleMedium,
)
: Text(
'稍后再看',
style: Theme.of(context).textTheme.titleMedium,
),
),
actions: [
TextButton(
onPressed: () => _laterController.toViewDel(),
child: const Text('移除已看'),
Obx(
() => _laterController.laterList.isNotEmpty
? TextButton(
onPressed: () => _laterController.toViewDel(),
child: const Text('移除已看'),
)
: const SizedBox(),
),
Obx(
() => _laterController.laterList.isNotEmpty
? IconButton(
tooltip: '一键清空',
onPressed: () => _laterController.toViewClear(),
icon: Icon(
Icons.clear_all_outlined,
size: 21,
color: Theme.of(context).colorScheme.primary,
),
)
: const SizedBox(),
),
// IconButton(
// tooltip: '一键清空',
// onPressed: () {},
// icon: Icon(
// Icons.clear_all_outlined,
// size: 21,
// color: Theme.of(context).colorScheme.primary,
// ),
// ),
const SizedBox(width: 8),
],
),
@@ -61,18 +74,31 @@ class _LaterPageState extends State<LaterPage> {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(
() => SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return VideoCardH(
videoItem: _laterController.laterList[index],
);
}, childCount: _laterController.laterList.length),
),
() => _laterController.laterList.isNotEmpty &&
!_laterController.isLoading.value
? SliverList(
delegate:
SliverChildBuilderDelegate((context, index) {
return VideoCardH(
videoItem: _laterController.laterList[index],
source: 'later',
);
}, childCount: _laterController.laterList.length),
)
: SliverToBoxAdapter(
child: Center(
child: Text(_laterController.isLoading.value
? '加载中'
: '没有数据'),
),
),
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
fn: () => setState(() {
_futureBuilderFuture = _laterController.queryLaterList();
}),
);
}
} else {
@@ -80,7 +106,7 @@ class _LaterPageState extends State<LaterPage> {
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
}, childCount: 5),
}, childCount: 10),
);
}
},

View File

@@ -22,10 +22,12 @@ class LivePage extends StatefulWidget {
class _LivePageState extends State<LivePage> {
final LiveController _liveController = Get.put(LiveController());
late Future _futureBuilderFuture;
@override
void initState() {
super.initState();
_futureBuilderFuture = _liveController.queryLiveList('init');
ScrollController scrollController = _liveController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
@@ -52,47 +54,58 @@ class _LivePageState extends State<LivePage> {
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () async {
return await _liveController.onRefresh();
},
child: CustomScrollView(
controller: _liveController.scrollController,
slivers: [
SliverPadding(
// 单列布局 EdgeInsets.zero
padding: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 0, StyleString.safeSpace, 0),
sliver: FutureBuilder(
future: _liveController.queryLiveList('init'),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(() =>
contentGrid(_liveController, _liveController.liveList));
return Container(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(StyleString.imgRadius),
),
child: RefreshIndicator(
onRefresh: () async {
return await _liveController.onRefresh();
},
child: CustomScrollView(
controller: _liveController.scrollController,
slivers: [
SliverPadding(
// 单列布局 EdgeInsets.zero
padding:
const EdgeInsets.fromLTRB(0, StyleString.safeSpace, 0, 0),
sliver: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return SliverLayoutBuilder(
builder: (context, boxConstraints) {
return Obx(() => contentGrid(
_liveController, _liveController.liveList));
});
} else {
return HttpError(
errMsg: data['msg'],
fn: () => {},
);
}
} else {
return HttpError(
errMsg: data['msg'],
fn: () => {},
);
// 缓存数据
if (_liveController.liveList.length > 1) {
return contentGrid(
_liveController, _liveController.liveList);
}
// 骨架屏
else {
return contentGrid(_liveController, []);
}
}
} else {
// 缓存数据
if (_liveController.liveList.length > 1) {
return contentGrid(
_liveController, _liveController.liveList);
}
// 骨架屏
else {
return contentGrid(_liveController, []);
}
}
},
},
),
),
),
const LoadingMore()
],
const LoadingMore()
],
),
),
);
}
@@ -100,22 +113,32 @@ class _LivePageState extends State<LivePage> {
OverlayEntry _createPopupDialog(liveItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
child: OverlayPop(videoItem: liveItem),
closeFn: _liveController.popupDialog?.remove,
child: OverlayPop(
videoItem: liveItem, closeFn: _liveController.popupDialog?.remove),
),
);
}
Widget contentGrid(ctr, liveList) {
double maxWidth = Get.size.width;
int baseWidth = 500;
int step = 300;
int crossAxisCount =
maxWidth > baseWidth ? 2 + ((maxWidth - baseWidth) / step).ceil() : 2;
if (maxWidth < 300) {
crossAxisCount = 1;
}
return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 行间距
mainAxisSpacing: StyleString.cardSpace + 2,
mainAxisSpacing: StyleString.cardSpace + 4,
// 列间距
crossAxisSpacing: StyleString.cardSpace + 3,
crossAxisSpacing: StyleString.cardSpace + 4,
// 列数
crossAxisCount: ctr.crossAxisCount,
crossAxisCount: crossAxisCount,
mainAxisExtent:
Get.size.width / ctr.crossAxisCount / StyleString.aspectRatio + 60,
Get.size.width / crossAxisCount / StyleString.aspectRatio + 66,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/pages/video/detail/reply/widgets/reply_item.dart';
import 'package:pilipala/utils/utils.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
@@ -35,11 +35,11 @@ class LiveCardV extends StatelessWidget {
longPress!();
}
},
onLongPressEnd: (details) {
if (longPressEnd != null) {
longPressEnd!();
}
},
// onLongPressEnd: (details) {
// if (longPressEnd != null) {
// longPressEnd!();
// }
// },
child: InkWell(
onTap: () async {
Get.toNamed('/liveRoom?roomid=${liveItem.roomId}',
@@ -103,7 +103,7 @@ class LiveContent extends StatelessWidget {
return Expanded(
child: Padding(
// 多列
padding: const EdgeInsets.fromLTRB(4, 5, 6, 6),
padding: const EdgeInsets.fromLTRB(4, 8, 0, 6),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -121,7 +121,12 @@ class LiveContent extends StatelessWidget {
),
Row(
children: [
const UpTag(),
const PBadge(
text: 'UP',
size: 'small',
stack: 'normal',
fs: 9,
),
Expanded(
child: Text(
liveItem.uname,
@@ -154,7 +159,7 @@ class VideoStat extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 45,
height: 50,
padding: const EdgeInsets.only(top: 22, left: 10, right: 10),
decoration: const BoxDecoration(
gradient: LinearGradient(

View File

@@ -1,4 +1,3 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/live.dart';
@@ -14,7 +13,7 @@ class LiveRoomController extends GetxController {
// 静音状态
RxBool volumeOff = false.obs;
PlPlayerController plPlayerController =
PlPlayerController(controlsEnabled: false);
PlPlayerController.getInstance(videoType: 'live');
// MeeduPlayerController meeduPlayerController = MeeduPlayerController(
// colorTheme: Theme.of(Get.context!).colorScheme.primary,
@@ -31,6 +30,9 @@ class LiveRoomController extends GetxController {
liveItem = Get.arguments['liveItem'];
heroTag = Get.arguments['heroTag'] ?? '';
if (liveItem.pic != null && liveItem.pic != '') {
cover = liveItem.pic;
}
if (liveItem.cover != null && liveItem.cover != '') {
cover = liveItem.cover;
}
}
@@ -72,12 +74,10 @@ class LiveRoomController extends GetxController {
if (value == 0) {
// 设置音量
volumeOff.value = false;
// meeduPlayerController.setVolume(volume);
} else {
// 取消音量
volume = value;
volumeOff.value = true;
// meeduPlayerController.setVolume(0);
}
}
}

View File

@@ -74,13 +74,13 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
),
],
),
actions: [
SizedBox(
height: 34,
child: ElevatedButton(onPressed: () {}, child: const Text('关注')),
),
const SizedBox(width: 12),
],
// actions: [
// SizedBox(
// height: 34,
// child: ElevatedButton(onPressed: () {}, child: const Text('关注')),
// ),
// const SizedBox(width: 12),
// ],
),
body: Column(
children: [
@@ -112,68 +112,67 @@ class _LiveRoomPageState extends State<LiveRoomPage> {
],
),
),
if (_liveRoomController.liveItem.watchedShow != null)
Container(
height: 45,
padding: const EdgeInsets.only(left: 12, right: 12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
border: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor.withOpacity(0.1)),
),
),
child: Row(children: <Widget>[
SizedBox(
width: 38,
height: 38,
child: IconButton(
onPressed: () {},
icon: const Icon(
Icons.subtitles_outlined,
size: 21,
),
),
),
const Spacer(),
SizedBox(
width: 38,
height: 38,
child: IconButton(
onPressed: () {},
icon: const Icon(
Icons.hd_outlined,
size: 20,
),
),
),
SizedBox(
width: 38,
height: 38,
child: IconButton(
onPressed: () => _liveRoomController
.setVolumn(plPlayerController!.volume.value),
icon: Obx(() => Icon(
_liveRoomController.volumeOff.value
? Icons.volume_off_outlined
: Icons.volume_up_outlined,
size: 21,
)),
),
),
SizedBox(
width: 38,
height: 38,
child: IconButton(
onPressed: () => {},
// plPlayerController!.goToFullscreen(context),
icon: const Icon(
Icons.fullscreen,
),
),
),
]),
),
// Container(
// height: 45,
// padding: const EdgeInsets.only(left: 12, right: 12),
// decoration: BoxDecoration(
// color: Theme.of(context).colorScheme.background,
// border: Border(
// bottom: BorderSide(
// color: Theme.of(context).dividerColor.withOpacity(0.1)),
// ),
// ),
// child: Row(children: <Widget>[
// SizedBox(
// width: 38,
// height: 38,
// child: IconButton(
// onPressed: () {},
// icon: const Icon(
// Icons.subtitles_outlined,
// size: 21,
// ),
// ),
// ),
// const Spacer(),
// SizedBox(
// width: 38,
// height: 38,
// child: IconButton(
// onPressed: () {},
// icon: const Icon(
// Icons.hd_outlined,
// size: 20,
// ),
// ),
// ),
// SizedBox(
// width: 38,
// height: 38,
// child: IconButton(
// onPressed: () => _liveRoomController
// .setVolumn(plPlayerController!.volume.value),
// icon: Obx(() => Icon(
// _liveRoomController.volumeOff.value
// ? Icons.volume_off_outlined
// : Icons.volume_up_outlined,
// size: 21,
// )),
// ),
// ),
// SizedBox(
// width: 38,
// height: 38,
// child: IconButton(
// onPressed: () => {},
// // plPlayerController!.goToFullscreen(context),
// icon: const Icon(
// Icons.fullscreen,
// ),
// ),
// ),
// ]),
// ),
],
),
);

View File

@@ -15,23 +15,35 @@ class MainController extends GetxController {
RxList navigationBars = [
{
'icon': const Icon(
Icons.motion_photos_on_outlined,
Icons.favorite_outline,
size: 21,
),
'label': "推荐",
'selectIcon': const Icon(
Icons.favorite,
size: 21,
),
'label': "首页",
},
{
'icon': const Icon(
Icons.bolt,
Icons.motion_photos_on_outlined,
size: 21,
),
'selectIcon': const Icon(
Icons.motion_photos_on,
size: 21,
),
'label': "动态",
},
{
'icon': const Icon(
Icons.folder_open_outlined,
Icons.folder_outlined,
size: 20,
),
'selectIcon': const Icon(
Icons.folder,
size: 21,
),
'label': "媒体库",
}
].obs;

View File

@@ -1,11 +1,10 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import './controller.dart';
@@ -95,6 +94,13 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
}
}
@override
void dispose() async {
await GStrorage.close();
EventBus().off(EventName.loginEvent);
super.dispose();
}
@override
Widget build(BuildContext context) {
Box localCache = GStrorage.localCache;
@@ -135,21 +141,17 @@ class _MainAppState extends State<MainApp> with SingleTickerProviderStateMixin {
initialData: true,
builder: (context, AsyncSnapshot snapshot) {
return AnimatedSlide(
curve: Curves.linear,
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOutCubicEmphasized,
duration: const Duration(milliseconds: 1000),
offset: Offset(0, snapshot.data ? 0 : 1),
child: BottomNavigationBar(
currentIndex: selectedIndex,
// type: BottomNavigationBarType.shifting,
selectedItemColor: Theme.of(context).colorScheme.primary,
unselectedItemColor:
Theme.of(context).colorScheme.onSurfaceVariant,
selectedFontSize: 12.4,
onTap: (value) => setIndex(value),
items: [
child: NavigationBar(
onDestinationSelected: (value) => setIndex(value),
selectedIndex: selectedIndex,
destinations: <Widget>[
..._mainController.navigationBars.map((e) {
return BottomNavigationBarItem(
return NavigationDestination(
icon: e['icon'],
selectedIcon: e['selectIcon'],
label: e['label'],
);
}).toList(),

View File

@@ -3,6 +3,7 @@ import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/media/index.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/utils.dart';
class MediaPage extends StatefulWidget {
@@ -14,13 +15,29 @@ class MediaPage extends StatefulWidget {
class _MediaPageState extends State<MediaPage>
with AutomaticKeepAliveClientMixin {
late MediaController mediaController;
late Future _futureBuilderFuture;
EventBus eventBus = EventBus();
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
mediaController = Get.put(MediaController());
_futureBuilderFuture = mediaController.queryFavFolder();
eventBus.on(EventName.loginEvent, (args) {
mediaController.userLogin.value = args['status'];
setState(() {
_futureBuilderFuture = mediaController.queryFavFolder();
});
});
}
@override
Widget build(BuildContext context) {
super.build(context);
final MediaController mediaController = Get.put(MediaController());
Color primary = Theme.of(context).colorScheme.primary;
return Scaffold(
appBar: AppBar(toolbarHeight: 30),
@@ -59,7 +76,7 @@ class _MediaPageState extends State<MediaPage>
),
),
],
Obx(() => mediaController.userLogin.value == true
Obx(() => mediaController.userLogin.value
? favFolder(mediaController, context)
: const SizedBox())
],
@@ -107,7 +124,11 @@ class _MediaPageState extends State<MediaPage>
),
),
trailing: IconButton(
onPressed: () => mediaController.queryFavFolder(),
onPressed: () {
setState(() {
_futureBuilderFuture = mediaController.queryFavFolder();
});
},
icon: const Icon(
Icons.refresh,
size: 20,
@@ -119,7 +140,7 @@ class _MediaPageState extends State<MediaPage>
width: double.infinity,
height: 170,
child: FutureBuilder(
future: mediaController.queryFavFolder(),
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;

View File

@@ -2,7 +2,6 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:loading_more_list/loading_more_list.dart';
import 'package:pilipala/common/widgets/pull_to_refresh_header.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import 'package:pilipala/models/member/archive.dart';
import 'package:pilipala/pages/member/archive/index.dart';
@@ -152,7 +151,6 @@ class LoadMoreListSource extends LoadingMoreBase<VListItemModel> {
if (res['status']) {
addAll(res['data'].list.vlist);
}
print(length);
if (length < res['data'].page['count']) {
isSuccess = true;
} else {

View File

@@ -120,7 +120,7 @@ class _MemberDynamicPanelState extends State<MemberDynamicPanel>
class LoadMoreListSource extends LoadingMoreBase<DynamicItemModel> {
final _dynamicController = Get.put(MemberDynamicPanelController());
// @override
@override
Future<bool> loadData([bool isloadMoreAction = false]) async {
bool isSuccess = false;
var res = await _dynamicController.getMemberDynamic();

View File

@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/models/user/info.dart';
import 'package:pilipala/models/user/stat.dart';
import 'package:pilipala/utils/storage.dart';
@@ -10,9 +12,11 @@ class MineController extends GetxController {
Rx<UserInfoData> userInfo = UserInfoData().obs;
// 用户状态 动态、关注、粉丝
Rx<UserStat> userStat = UserStat().obs;
Box user = GStrorage.user;
RxBool userLogin = false.obs;
Box user = GStrorage.user;
Box setting = GStrorage.setting;
Box userInfoCache = GStrorage.userInfo;
Rx<ThemeType> themeType = ThemeType.system.obs;
@override
onInit() {
@@ -21,13 +25,13 @@ class MineController extends GetxController {
if (userInfoCache.get('userInfoCache') != null) {
userInfo.value = userInfoCache.get('userInfoCache');
}
themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode,
defaultValue: ThemeType.system.code)];
}
onLogin() async {
if (!userLogin.value) {
/// TODO
Get.back();
await Future.delayed(const Duration(milliseconds: 150));
Get.toNamed(
'/webview',
parameters: {
@@ -90,4 +94,31 @@ class MineController extends GetxController {
userLogin.value = false;
// Get.find<MainController>().resetLast();
}
onChangeTheme() {
Brightness currentBrightness =
MediaQuery.of(Get.context!).platformBrightness;
ThemeType currentTheme = themeType.value;
switch (currentTheme) {
case ThemeType.dark:
setting.put(SettingBoxKey.themeMode, ThemeType.light.code);
themeType.value = ThemeType.light;
break;
case ThemeType.light:
setting.put(SettingBoxKey.themeMode, ThemeType.dark.code);
themeType.value = ThemeType.dark;
break;
case ThemeType.system:
// 判断当前的颜色模式
if (currentBrightness == Brightness.light) {
setting.put(SettingBoxKey.themeMode, ThemeType.dark.code);
themeType.value = ThemeType.dark;
} else {
setting.put(SettingBoxKey.themeMode, ThemeType.light.code);
themeType.value = ThemeType.light;
}
break;
}
Get.forceAppUpdate();
}
}

View File

@@ -5,15 +5,38 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'controller.dart';
class MinePage extends StatelessWidget {
class MinePage extends StatefulWidget {
const MinePage({super.key});
@override
Widget build(BuildContext context) {
final MineController mineController = Get.put(MineController());
State<MinePage> createState() => _MinePageState();
}
class _MinePageState extends State<MinePage> {
final MineController mineController = Get.put(MineController());
late Future _futureBuilderFuture;
EventBus eventBus = EventBus();
@override
void initState() {
super.initState();
_futureBuilderFuture = mineController.queryUserInfo();
eventBus.on(EventName.loginEvent, (args) {
mineController.userLogin.value = args['status'];
if (mounted) {
setState(() {
_futureBuilderFuture = mineController.queryUserInfo();
});
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
@@ -21,16 +44,23 @@ class MinePage extends StatelessWidget {
elevation: 0,
toolbarHeight: kTextTabBarHeight + 20,
backgroundColor: Colors.transparent,
title: null,
centerTitle: false,
title: const Text(
'PLPL',
style: TextStyle(
height: 2.8,
fontSize: 17,
fontWeight: FontWeight.bold,
fontFamily: 'Jura-Bold',
),
),
actions: [
IconButton(
onPressed: () {
Get.changeThemeMode(ThemeMode.dark);
},
onPressed: () => mineController.onChangeTheme(),
icon: Icon(
Get.theme == ThemeData.light()
? CupertinoIcons.moon
: CupertinoIcons.sun_max,
mineController.themeType.value == ThemeType.dark
? CupertinoIcons.sun_max
: CupertinoIcons.moon,
size: 22,
),
),
@@ -53,7 +83,7 @@ class MinePage extends StatelessWidget {
children: [
const SizedBox(height: 10),
FutureBuilder(
future: mineController.queryUserInfo(),
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data['status']) {
@@ -93,7 +123,7 @@ class MinePage extends StatelessWidget {
src: _mineController.userInfo.value.face,
width: 85,
height: 85)
: Image.asset('assets/images/loading.png'),
: Image.asset('assets/images/noface.jpeg'),
),
),
),

View File

@@ -132,6 +132,7 @@ class _ImagePreviewState extends State<ImagePreview>
_doubleClickAnimationController.forward();
},
// ignore: body_might_complete_normally_nullable
loadStateChanged: (ExtendedImageState state) {
if (state.extendedImageLoadState == LoadState.loading) {
final ImageChunkEvent? loadingProgress =

View File

@@ -2,17 +2,14 @@ import 'package:flutter/cupertino.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/models/model_rec_video_item.dart';
import 'package:pilipala/models/home/rcmd/result.dart';
import 'package:pilipala/utils/storage.dart';
class RcmdController extends GetxController {
final ScrollController scrollController = ScrollController();
int count = 12;
int _currentPage = 1;
int crossAxisCount = 2;
RxList<RecVideoItemModel> videoList = [RecVideoItemModel()].obs;
bool isLoadingMore = false;
bool flag = false;
int _currentPage = 0;
RxList<RecVideoItemAppModel> videoList = <RecVideoItemAppModel>[].obs;
bool isLoadingMore = true;
OverlayEntry? popupDialog;
Box recVideo = GStrorage.recVideo;
@@ -21,7 +18,7 @@ class RcmdController extends GetxController {
super.onInit();
if (recVideo.get('cacheList') != null &&
recVideo.get('cacheList').isNotEmpty) {
List<RecVideoItemModel> list = [];
List<RecVideoItemAppModel> list = [];
for (var i in recVideo.get('cacheList')) {
list.add(i);
}
@@ -31,13 +28,18 @@ class RcmdController extends GetxController {
// 获取推荐
Future queryRcmdFeed(type) async {
var res = await VideoHttp.rcmdVideoList(
ps: count,
if (isLoadingMore == false) {
return;
}
if (type == 'onRefresh') {
_currentPage = 0;
}
var res = await VideoHttp.rcmdVideoListApp(
freshIdx: _currentPage,
);
if (res['status']) {
if (type == 'init') {
if (videoList.length > 1) {
if (videoList.isNotEmpty) {
videoList.addAll(res['data']);
} else {
videoList.value = res['data'];
@@ -56,6 +58,7 @@ class RcmdController extends GetxController {
// 下拉刷新
Future onRefresh() async {
isLoadingMore = true;
queryRcmdFeed('onRefresh');
}

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
@@ -23,6 +24,7 @@ class RcmdPage extends StatefulWidget {
class _RcmdPageState extends State<RcmdPage>
with AutomaticKeepAliveClientMixin {
final RcmdController _rcmdController = Get.put(RcmdController());
late Future _futureBuilderFuture;
@override
bool get wantKeepAlive => true;
@@ -30,6 +32,7 @@ class _RcmdPageState extends State<RcmdPage>
@override
void initState() {
super.initState();
_futureBuilderFuture = _rcmdController.queryRcmdFeed('init');
ScrollController scrollController = _rcmdController.scrollController;
StreamController<bool> mainStream =
Get.find<MainController>().bottomBarStream;
@@ -39,7 +42,9 @@ class _RcmdPageState extends State<RcmdPage>
scrollController.position.maxScrollExtent - 200) {
if (!_rcmdController.isLoadingMore) {
_rcmdController.isLoadingMore = true;
_rcmdController.onLoad();
WidgetsBinding.instance.addPostFrameCallback((_) async {
_rcmdController.onLoad();
});
}
}
@@ -57,78 +62,103 @@ class _RcmdPageState extends State<RcmdPage>
@override
Widget build(BuildContext context) {
super.build(context);
return RefreshIndicator(
onRefresh: () async {
return await _rcmdController.onRefresh();
},
child: CustomScrollView(
controller: _rcmdController.scrollController,
slivers: [
SliverPadding(
// 单列布局 EdgeInsets.zero
padding: _rcmdController.crossAxisCount == 1
? EdgeInsets.zero
: const EdgeInsets.fromLTRB(
StyleString.safeSpace, 0, StyleString.safeSpace, 0),
sliver: FutureBuilder(
future: _rcmdController.queryRcmdFeed('init'),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Obx(() => contentGrid(
_rcmdController, _rcmdController.videoList));
return Container(
clipBehavior: Clip.hardEdge,
margin: const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace),
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(StyleString.imgRadius),
),
child: RefreshIndicator(
onRefresh: () async {
return await _rcmdController.onRefresh();
},
child: CustomScrollView(
controller: _rcmdController.scrollController,
slivers: [
SliverPadding(
padding:
const EdgeInsets.fromLTRB(0, StyleString.safeSpace, 0, 0),
sliver: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return Platform.isAndroid || Platform.isIOS
? Obx(() => contentGrid(
_rcmdController, _rcmdController.videoList))
: SliverLayoutBuilder(
builder: (context, boxConstraints) {
return Obx(() => contentGrid(
_rcmdController, _rcmdController.videoList));
});
} else {
return HttpError(
errMsg: data['msg'],
fn: () {
setState(() {
_futureBuilderFuture =
_rcmdController.queryRcmdFeed('init');
});
},
);
}
} else {
return HttpError(
errMsg: data['msg'],
fn: () => {},
);
// 缓存数据
if (_rcmdController.videoList.isNotEmpty) {
return contentGrid(
_rcmdController, _rcmdController.videoList);
}
// 骨架屏
else {
return contentGrid(_rcmdController, []);
}
}
} else {
// 缓存数据
if (_rcmdController.videoList.length > 1) {
return contentGrid(
_rcmdController, _rcmdController.videoList);
}
// 骨架屏
else {
return contentGrid(_rcmdController, []);
}
}
},
},
),
),
),
const LoadingMore()
],
const LoadingMore()
],
),
),
);
}
OverlayEntry _createPopupDialog(videoItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
child: OverlayPop(videoItem: videoItem),
));
builder: (context) => AnimatedDialog(
closeFn: _rcmdController.popupDialog?.remove,
child: OverlayPop(
videoItem: videoItem, closeFn: _rcmdController.popupDialog?.remove),
),
);
}
Widget contentGrid(ctr, videoList) {
double maxWidth = Get.size.width;
int baseWidth = 500;
int step = 300;
int crossAxisCount =
maxWidth > baseWidth ? 2 + ((maxWidth - baseWidth) / step).ceil() : 2;
if (maxWidth < 300) {
crossAxisCount = 1;
}
return SliverGrid(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
// 行间距
mainAxisSpacing: StyleString.cardSpace + 2,
mainAxisSpacing: StyleString.cardSpace + 4,
// 列间距
crossAxisSpacing: StyleString.cardSpace + 3,
crossAxisSpacing: StyleString.cardSpace + 4,
// 列数
crossAxisCount: ctr.crossAxisCount,
crossAxisCount: crossAxisCount,
mainAxisExtent:
Get.size.width / ctr.crossAxisCount / StyleString.aspectRatio + 60,
Get.size.width / crossAxisCount / StyleString.aspectRatio + 66,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return videoList!.isNotEmpty
?
// VideoCardV(videoItem: videoList![index])
VideoCardV(
? VideoCardV(
videoItem: videoList[index],
longPress: () {
_rcmdController.popupDialog =

View File

@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/index.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/search/suggest.dart';
@@ -20,10 +21,12 @@ class SSearchController extends GetxController {
final _debouncer =
Debouncer(delay: const Duration(milliseconds: 200)); // 设置延迟时间
String hintText = '搜索';
RxString defaultSearch = '输入关键词搜索'.obs;
@override
void onInit() {
super.onInit();
searchDefault();
if (hotKeyword.get('cacheList') != null &&
hotKeyword.get('cacheList').isNotEmpty) {
List<HotSearchItem> list = [];
@@ -56,7 +59,7 @@ class SSearchController extends GetxController {
}
void onClear() {
if (searchKeyWord.value.isNotEmpty) {
if (searchKeyWord.value.isNotEmpty && controller.value.text != '') {
controller.value.clear();
searchKeyWord.value = '';
searchSuggestList.value = [];
@@ -121,4 +124,12 @@ class SSearchController extends GetxController {
historyList.refresh();
histiryWord.put('cacheList', []);
}
void searchDefault() async {
var res = await Request().get(Api.searchDefault);
if (res.data['code'] == 0) {
searchKeyWord.value =
hintText = defaultSearch.value = res.data['data']['name'];
}
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:animations/animations.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'controller.dart';
@@ -41,61 +42,117 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
shape: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor.withOpacity(0.08),
width: 1,
return OpenContainer(
closedElevation: 0,
openElevation: 0,
openColor: Theme.of(context).colorScheme.background,
middleColor: Theme.of(context).colorScheme.background,
closedColor: Theme.of(context).colorScheme.background,
closedShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(30.0))),
openShape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(30.0))),
closedBuilder: (BuildContext context, VoidCallback openContainer) {
return Container(
width: 250,
height: 44,
clipBehavior: Clip.hardEdge,
decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(25)),
),
),
titleSpacing: 0,
actions: [
Hero(
tag: 'searchTag',
child: IconButton(
onPressed: () => _searchController.submit(),
icon: const Icon(CupertinoIcons.search, size: 22)),
),
const SizedBox(width: 10)
],
title: Obx(
() => TextField(
autofocus: true,
focusNode: _searchController.searchFocusNode,
controller: _searchController.controller.value,
textInputAction: TextInputAction.search,
onChanged: (value) => _searchController.onChange(value),
decoration: InputDecoration(
hintText: _searchController.hintText,
border: InputBorder.none,
suffixIcon: IconButton(
icon: Icon(
Icons.clear,
size: 22,
color: Theme.of(context).colorScheme.outline,
),
onPressed: () => _searchController.onClear(),
child: Material(
color:
Theme.of(context).colorScheme.secondaryContainer.withAlpha(115),
child: InkWell(
splashColor: Theme.of(context)
.colorScheme
.primaryContainer
.withOpacity(0.3),
onTap: openContainer,
child: Row(
children: [
const SizedBox(width: 14),
Icon(
Icons.search_outlined,
color: Theme.of(context).colorScheme.onSecondaryContainer,
),
const SizedBox(width: 10),
Expanded(
child: Obx(
() => Text(
_searchController.defaultSearch.value,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
),
],
),
),
onSubmitted: (String value) => _searchController.submit(),
),
),
),
body: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 12),
// 搜索建议
_searchSuggest(),
// 热搜
hotSearch(),
// 搜索历史
_history()
],
),
),
);
},
openBuilder: (BuildContext context, VoidCallback _) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
shape: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor.withOpacity(0.08),
width: 1,
),
),
titleSpacing: 0,
actions: [
Hero(
tag: 'searchTag',
child: IconButton(
onPressed: () => _searchController.submit(),
icon: const Icon(CupertinoIcons.search, size: 22)),
),
const SizedBox(width: 10)
],
title: Obx(
() => TextField(
autofocus: true,
focusNode: _searchController.searchFocusNode,
controller: _searchController.controller.value,
textInputAction: TextInputAction.search,
onChanged: (value) => _searchController.onChange(value),
decoration: InputDecoration(
hintText: _searchController.hintText,
border: InputBorder.none,
suffixIcon: IconButton(
icon: Icon(
Icons.clear,
size: 22,
color: Theme.of(context).colorScheme.outline,
),
onPressed: () => _searchController.onClear(),
),
),
onSubmitted: (String value) => _searchController.submit(),
),
),
),
body: SingleChildScrollView(
child: Column(
children: [
const SizedBox(height: 12),
// 搜索建议
_searchSuggest(),
// 热搜
hotSearch(),
// 搜索历史
_history()
],
),
),
);
},
);
}
@@ -211,13 +268,13 @@ class _SearchPageState extends State<SearchPage> with RouteAware {
return Obx(
() => Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(10, 25, 4, 0),
padding: const EdgeInsets.fromLTRB(10, 25, 6, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_searchController.historyList.isNotEmpty)
Padding(
padding: const EdgeInsets.fromLTRB(6, 0, 1, 2),
padding: const EdgeInsets.fromLTRB(6, 0, 0, 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [

View File

@@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/utils.dart';
class SearchPanelController extends GetxController {
SearchPanelController({this.keyword, this.searchType});
@@ -21,6 +23,7 @@ class SearchPanelController extends GetxController {
} else if (type == 'onRefresh') {
resultList.value = result['data'].list;
}
onPushDetail(keyword, resultList);
}
return result;
}
@@ -40,4 +43,24 @@ class SearchPanelController extends GetxController {
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
}
}
void onPushDetail(keyword, resultList) async {
// 匹配输入内容如果是AV、BV号且有结果 直接跳转详情页
Map matchRes = IdUtils.matchAvorBv(input: keyword);
List matchKeys = matchRes.keys.toList();
if (matchKeys.isNotEmpty && searchType == SearchType.video) {
String bvid = resultList.first.bvid;
int aid = resultList.first.aid;
String heroTag = Utils.makeHeroTag(bvid);
int cid = await SearchHttp.ab2c(aid: aid, bvid: bvid);
if (matchKeys.first == 'BV' && matchRes[matchKeys.first] == bvid ||
matchKeys.first == 'AV' && matchRes[matchKeys.first] == aid) {
Get.toNamed(
'/video?bvid=$bvid&cid=$cid',
arguments: {'videoItem': resultList.first, 'heroTag': heroTag},
);
}
}
}
}

View File

@@ -25,7 +25,7 @@ class SearchPanel extends StatefulWidget {
class _SearchPanelState extends State<SearchPanel>
with AutomaticKeepAliveClientMixin {
late SearchPanelController? _searchPanelController;
late SearchPanelController _searchPanelController;
bool _isLoadingMore = false;
late Future _futureBuilderFuture;
@@ -41,21 +41,20 @@ class _SearchPanelState extends State<SearchPanel>
keyword: widget.keyword,
searchType: widget.searchType,
),
tag: widget.searchType!.type + widget.tag!,
tag: widget.searchType!.type,
);
ScrollController scrollController =
_searchPanelController!.scrollController;
ScrollController scrollController = _searchPanelController.scrollController;
scrollController.addListener(() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 100) {
if (!_isLoadingMore) {
_isLoadingMore = true;
await _searchPanelController!.onSearch(type: 'onLoad');
await _searchPanelController.onSearch(type: 'onLoad');
_isLoadingMore = false;
}
}
});
_futureBuilderFuture = _searchPanelController!.onSearch();
_futureBuilderFuture = _searchPanelController.onSearch();
}
@override
@@ -63,7 +62,7 @@ class _SearchPanelState extends State<SearchPanel>
super.build(context);
return RefreshIndicator(
onRefresh: () async {
await _searchPanelController!.onRefresh();
await _searchPanelController.onRefresh();
},
child: FutureBuilder(
future: _futureBuilderFuture,
@@ -71,7 +70,7 @@ class _SearchPanelState extends State<SearchPanel>
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data;
var ctr = _searchPanelController;
List list = ctr!.resultList;
List list = ctr.resultList;
if (data['status']) {
return Obx(() {
switch (widget.searchType) {

View File

@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/constants.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/utils/utils.dart';
@@ -32,6 +33,7 @@ class LiveItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
String heroTag = Utils.makeHeroTag(liveItem.roomid);
return Card(
elevation: 0,
clipBehavior: Clip.hardEdge,
@@ -40,7 +42,10 @@ class LiveItem extends StatelessWidget {
),
margin: EdgeInsets.zero,
child: InkWell(
onTap: () {},
onTap: () async {
Get.toNamed('/liveRoom?roomid=${liveItem.roomid}',
arguments: {'liveItem': liveItem, 'heroTag': heroTag});
},
child: Column(
children: [
ClipRRect(
@@ -58,7 +63,7 @@ class LiveItem extends StatelessWidget {
return Stack(
children: [
Hero(
tag: Utils.makeHeroTag(liveItem.roomid),
tag: heroTag,
child: NetworkImgLayer(
src: liveItem.cover,
type: 'emote',

View File

@@ -41,8 +41,13 @@ Widget searchMbangumiPanel(BuildContext context, ctr, list) {
height: 148,
src: i.cover,
),
pBadge(i.mediaType == 1 ? '番剧' : '国创', context, 6.0, 4.0,
null, null)
PBadge(
text: i.mediaType == 1 ? '番剧' : '国创',
top: 6.0,
right: 4.0,
bottom: null,
left: null,
)
],
),
const SizedBox(width: 10),

View File

@@ -88,6 +88,7 @@ class _SearchResultPageState extends State<SearchResultPage>
tag: SearchType.values[index].type)
.animateToTop();
}
_searchResultController!.tabIndex = index;
},
),

View File

@@ -1,16 +1,24 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/mine/controller.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
class SettingController extends GetxController {
Box user = GStrorage.user;
RxBool userLogin = false.obs;
Box userInfoCache = GStrorage.userInfo;
Box setting = GStrorage.setting;
Box userInfoCache = GStrorage.userInfo;
RxBool userLogin = false.obs;
RxBool feedBackEnable = false.obs;
RxInt picQuality = 10.obs;
Rx<ThemeType> themeType = ThemeType.system.obs;
@override
void onInit() {
@@ -18,13 +26,54 @@ class SettingController extends GetxController {
userLogin.value = user.get(UserBoxKey.userLogin) ?? false;
feedBackEnable.value =
setting.get(SettingBoxKey.feedBackEnable, defaultValue: false);
picQuality.value =
setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);
themeType.value = ThemeType.values[setting.get(SettingBoxKey.themeMode,
defaultValue: ThemeType.system.code)];
}
loginOut() async {
await Request.removeCookie();
await Get.find<MineController>().resetUserInfo();
userLogin.value = user.get(UserBoxKey.userLogin) ?? false;
userInfoCache.put('userInfoCache', null);
SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('确认要退出登录吗'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
child: const Text('点错了'),
),
TextButton(
onPressed: () async {
// 清空cookie
await Request.cookieManager.cookieJar.deleteAll();
Request.dio.options.headers['cookie'] = '';
// 清空本地存储的用户标识
userInfoCache.put('userInfoCache', null);
user.put(UserBoxKey.accessKey, {'mid': -1, 'value': ''});
// 更改我的页面登录状态
await Get.find<MineController>().resetUserInfo();
// 更改主页登录状态
HomeController homeCtr = Get.find<HomeController>();
homeCtr.updateLoginStatus(false);
// 事件通知
EventBus eventBus = EventBus();
eventBus.emit(EventName.loginEvent, {'status': false});
SmartDialog.dismiss().then((value) => Get.back());
},
child: const Text('确认'),
)
],
);
},
);
}
// 开启关闭震动反馈

View File

@@ -0,0 +1,170 @@
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/plugin/pl_player/models/fullscreen_mode.dart';
import 'package:pilipala/utils/storage.dart';
import 'widgets/switch_item.dart';
class PlaySetting extends StatefulWidget {
const PlaySetting({super.key});
@override
State<PlaySetting> createState() => _PlaySettingState();
}
class _PlaySettingState extends State<PlaySetting> {
Box setting = GStrorage.setting;
late dynamic defaultVideoQa;
late dynamic defaultAudioQa;
late dynamic defaultDecode;
late int defaultFullScreenMode;
@override
void initState() {
super.initState();
defaultVideoQa = setting.get(SettingBoxKey.defaultVideoQa,
defaultValue: VideoQuality.values.last.code);
defaultAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
defaultValue: AudioQuality.values.last.code);
defaultDecode = setting.get(SettingBoxKey.defaultDecode,
defaultValue: VideoDecodeFormats.values.last.code);
defaultFullScreenMode = setting.get(SettingBoxKey.fullScreenMode,
defaultValue: FullScreenMode.values.first.code);
}
@override
Widget build(BuildContext context) {
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
TextStyle subTitleStyle = Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline);
return Scaffold(
appBar: AppBar(
centerTitle: false,
titleSpacing: 0,
title: Text(
'播放设置',
style: Theme.of(context).textTheme.titleMedium,
),
),
body: ListView(
children: [
const SetSwitchItem(
title: '自动播放',
subTitle: '进入详情页自动播放',
setKey: SettingBoxKey.autoPlayEnable,
defaultVal: true,
),
const SetSwitchItem(
title: '开启硬解',
subTitle: '以较低功耗播放视频',
setKey: SettingBoxKey.enableHA,
defaultVal: true,
),
ListTile(
dense: false,
title: Text('默认画质', style: titleStyle),
subtitle: Text(
'当前画质${VideoQualityCode.fromCode(defaultVideoQa)!.description!}',
style: subTitleStyle,
),
trailing: PopupMenuButton(
initialValue: defaultVideoQa,
icon: const Icon(Icons.more_vert_outlined, size: 22),
onSelected: (item) {
defaultVideoQa = item;
setting.put(SettingBoxKey.defaultVideoQa, item);
setState(() {});
},
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
for (var i in VideoQuality.values.reversed) ...[
PopupMenuItem(
value: i.code,
child: Text(i.description),
),
]
],
),
),
ListTile(
dense: false,
title: Text('默认音质', style: titleStyle),
subtitle: Text(
'当前音质${AudioQualityCode.fromCode(defaultAudioQa)!.description!}',
style: subTitleStyle,
),
trailing: PopupMenuButton(
initialValue: defaultAudioQa,
icon: const Icon(Icons.more_vert_outlined, size: 22),
onSelected: (item) {
defaultAudioQa = item;
setting.put(SettingBoxKey.defaultAudioQa, item);
setState(() {});
},
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
for (var i in AudioQuality.values.reversed) ...[
PopupMenuItem(
value: i.code,
child: Text(i.description),
),
]
],
),
),
ListTile(
dense: false,
title: Text('默认解码格式', style: titleStyle),
subtitle: Text(
'当前解码格式${VideoDecodeFormatsCode.fromCode(defaultDecode)!.description!}',
style: subTitleStyle,
),
trailing: PopupMenuButton(
initialValue: defaultDecode,
icon: const Icon(Icons.more_vert_outlined, size: 22),
onSelected: (item) {
defaultDecode = item;
setting.put(SettingBoxKey.defaultDecode, item);
setState(() {});
},
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
for (var i in VideoDecodeFormats.values) ...[
PopupMenuItem(
value: i.code,
child: Text(i.description),
),
]
],
),
),
ListTile(
dense: false,
title: Text('默认全屏方式', style: titleStyle),
subtitle: Text(
'当前全屏方式:${FullScreenModeCode.fromCode(defaultFullScreenMode)!.description}',
style: subTitleStyle,
),
trailing: PopupMenuButton(
initialValue: defaultFullScreenMode,
icon: const Icon(Icons.more_vert_outlined, size: 22),
onSelected: (item) {
defaultFullScreenMode = item;
setting.put(SettingBoxKey.fullScreenMode, item);
setState(() {});
},
itemBuilder: (BuildContext context) => <PopupMenuEntry>[
for (var i in FullScreenMode.values) ...[
PopupMenuItem(
value: i.code,
child: Text(i.description),
),
]
],
),
),
],
),
);
}
}

View File

@@ -0,0 +1,58 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/utils/storage.dart';
class PrivacySetting extends StatefulWidget {
const PrivacySetting({super.key});
@override
State<PrivacySetting> createState() => _PrivacySettingState();
}
class _PrivacySettingState extends State<PrivacySetting> {
bool userLogin = false;
Box user = GStrorage.user;
@override
void initState() {
super.initState();
userLogin = user.get(UserBoxKey.userLogin) ?? false;
}
@override
Widget build(BuildContext context) {
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
TextStyle subTitleStyle = Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline);
return Scaffold(
appBar: AppBar(
centerTitle: false,
titleSpacing: 0,
title: Text(
'隐私设置',
style: Theme.of(context).textTheme.titleMedium,
),
),
body: Column(
children: [
ListTile(
onTap: () {
if (!userLogin) {
SmartDialog.showToast('登录后查看');
return;
}
Get.toNamed('/blackListPage');
},
dense: false,
title: Text('黑名单管理', style: titleStyle),
subtitle: Text('已拉黑用户', style: subTitleStyle),
),
],
),
);
}
}

View File

@@ -0,0 +1,191 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/common/theme_type.dart';
import 'package:pilipala/utils/storage.dart';
import 'controller.dart';
class StyleSetting extends StatefulWidget {
const StyleSetting({super.key});
@override
State<StyleSetting> createState() => _StyleSettingState();
}
class _StyleSettingState extends State<StyleSetting> {
final SettingController settingController = Get.put(SettingController());
Box setting = GStrorage.setting;
late int picQuality;
late ThemeType _tempThemeValue;
@override
void initState() {
super.initState();
picQuality = setting.get(SettingBoxKey.defaultPicQa, defaultValue: 10);
_tempThemeValue = settingController.themeType.value;
}
@override
Widget build(BuildContext context) {
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
TextStyle subTitleStyle = Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline);
return Scaffold(
appBar: AppBar(
centerTitle: false,
titleSpacing: 0,
title: Text(
'外观设置',
style: Theme.of(context).textTheme.titleMedium,
),
),
body: ListView(
children: [
Obx(
() => ListTile(
enableFeedback: true,
onTap: () => settingController.onOpenFeedBack(),
title: const Text('震动反馈'),
subtitle: Text('请确定手机设置中已开启震动反馈', style: subTitleStyle),
trailing: Transform.scale(
scale: 0.8,
child: Switch(
thumbIcon: MaterialStateProperty.resolveWith<Icon?>(
(Set<MaterialState> states) {
if (states.isNotEmpty &&
states.first == MaterialState.selected) {
return const Icon(Icons.done);
}
return null; // All other states will use the default thumbIcon.
}),
value: settingController.feedBackEnable.value,
onChanged: (value) => settingController.onOpenFeedBack()),
),
),
),
ListTile(
dense: false,
onTap: () {
showDialog(
context: context,
builder: (context) {
return StatefulBuilder(
builder: (context, StateSetter setState) {
final SettingController settingController =
Get.put(SettingController());
return AlertDialog(
title: const Text('图片质量'),
contentPadding: const EdgeInsets.only(
top: 20, left: 8, right: 8, bottom: 8),
content: SizedBox(
height: 40,
child: Slider(
value: picQuality.toDouble(),
min: 10,
max: 100,
divisions: 9,
label: '$picQuality%',
onChanged: (double val) {
picQuality = val.toInt();
setState(() {});
},
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('取消',
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline))),
TextButton(
onPressed: () {
setting.put(
SettingBoxKey.defaultPicQa, picQuality);
Get.back();
settingController.picQuality.value = picQuality;
},
child: const Text('确定'),
)
],
);
},
);
},
);
},
title: Text('图片质量', style: titleStyle),
subtitle: Text('选择合适的图片清晰度上限100%', style: subTitleStyle),
trailing: Obx(
() => Text(
'${settingController.picQuality.value}%',
style: Theme.of(context).textTheme.titleSmall,
),
),
),
ListTile(
dense: false,
onTap: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('主题模式'),
contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12),
content: StatefulBuilder(
builder: (context, StateSetter setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
for (var i in ThemeType.values) ...[
RadioListTile(
value: i,
title: Text(i.description, style: titleStyle),
groupValue: _tempThemeValue,
onChanged: (ThemeType? value) {
setState(() {
_tempThemeValue = i;
});
},
),
]
],
);
}),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline),
)),
TextButton(
onPressed: () {
settingController.themeType.value = _tempThemeValue;
setting.put(
SettingBoxKey.themeMode, _tempThemeValue.code);
Get.forceAppUpdate();
Get.back();
},
child: const Text('确定'))
],
);
},
);
},
title: Text('主题模式', style: titleStyle),
subtitle: Obx(() => Text(
'当前模式:${settingController.themeType.value.description}',
style: subTitleStyle)),
trailing: const Icon(Icons.arrow_right_alt_outlined),
),
],
),
);
}
}

View File

@@ -7,39 +7,38 @@ class SettingPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
TextStyle subTitleStyle = Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline);
final SettingController settingController = Get.put(SettingController());
return Scaffold(
appBar: AppBar(
title: const Text('设置'),
centerTitle: false,
titleSpacing: 0,
title: Text(
'设置',
style: Theme.of(context).textTheme.titleMedium,
),
),
body: Column(
children: [
Obx(
() => ListTile(
enableFeedback: true,
onTap: () => settingController.onOpenFeedBack(),
title: const Text('震动反馈'),
subtitle: Text('请确定手机设置中已开启震动反馈', style: subTitleStyle),
trailing: Transform.scale(
scale: 0.8,
child: Switch(
thumbIcon: MaterialStateProperty.resolveWith<Icon?>(
(Set<MaterialState> states) {
if (states.isNotEmpty &&
states.first == MaterialState.selected) {
return const Icon(Icons.done);
}
return null; // All other states will use the default thumbIcon.
}),
value: settingController.feedBackEnable.value,
onChanged: (value) => settingController.onOpenFeedBack()),
),
),
ListTile(
onTap: () => Get.toNamed('/privacySetting'),
dense: false,
title: const Text('隐私设置'),
),
ListTile(
onTap: () => Get.toNamed('/playSetting'),
dense: false,
title: const Text('播放设置'),
),
ListTile(
onTap: () => Get.toNamed('/styleSetting'),
dense: false,
title: const Text('外观设置'),
),
// ListTile(
// onTap: () {},
// dense: false,
// title: const Text('其他设置'),
// ),
Obx(
() => Visibility(
visible: settingController.userLogin.value,
@@ -50,6 +49,11 @@ class SettingPage extends StatelessWidget {
),
),
),
ListTile(
onTap: () => Get.toNamed('/about'),
dense: false,
title: const Text('关于'),
),
],
),
);

View File

@@ -0,0 +1,120 @@
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/models/video/play/quality.dart';
import 'package:pilipala/utils/storage.dart';
class SetSelectItem extends StatefulWidget {
final String? title;
final String? subTitle;
final String? setKey;
const SetSelectItem({
this.title,
this.subTitle,
this.setKey,
Key? key,
}) : super(key: key);
@override
State<SetSelectItem> createState() => _SetSelectItemState();
}
class _SetSelectItemState extends State<SetSelectItem> {
Box setting = GStrorage.setting;
late dynamic currentVal;
late int currentIndex;
late List menus;
late List<PopupMenuEntry> popMenuItems;
@override
void initState() {
super.initState();
late String defaultVal;
switch (widget.setKey) {
case 'defaultVideoQa':
defaultVal = VideoQuality.values.last.description;
List<VideoQuality> list = menus = VideoQuality.values.reversed.toList();
currentVal = setting.get(widget.setKey, defaultValue: defaultVal);
currentIndex =
list.firstWhere((i) => i.description == currentVal).index;
popMenuItems = [
for (var i in list) ...[
PopupMenuItem(
value: i.code,
child: Text(i.description),
)
]
];
break;
case 'defaultAudioQa':
defaultVal = AudioQuality.values.last.description;
List<AudioQuality> list = menus = AudioQuality.values.reversed.toList();
currentVal = setting.get(widget.setKey, defaultValue: defaultVal);
currentIndex =
list.firstWhere((i) => i.description == currentVal).index;
popMenuItems = [
for (var i in list) ...[
PopupMenuItem(
value: i.index,
child: Text(i.description),
),
]
];
break;
case 'defaultDecode':
defaultVal = VideoDecodeFormats.values[0].description;
currentVal = setting.get(widget.setKey, defaultValue: defaultVal);
List<VideoDecodeFormats> list = menus = VideoDecodeFormats.values;
currentIndex =
list.firstWhere((i) => i.description == currentVal).index;
popMenuItems = [
for (var i in list) ...[
PopupMenuItem(
value: i.index,
child: Text(i.description),
),
]
];
break;
case 'defaultVideoSpeed':
defaultVal = '1.0';
currentVal = setting.get(widget.setKey, defaultValue: defaultVal);
break;
}
}
@override
Widget build(BuildContext context) {
TextStyle subTitleStyle = Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline);
return ListTile(
onTap: () {},
dense: false,
title: Text(widget.title!),
subtitle: Text(
'当前${widget.title!} $currentVal',
style: subTitleStyle,
),
trailing: PopupMenuButton(
initialValue: currentIndex,
icon: const Icon(
Icons.arrow_forward_rounded,
size: 22,
),
onSelected: (item) {
currentVal = menus.firstWhere((e) => e.code == item).first;
setState(() {});
},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry>[...popMenuItems],
),
);
}
}

View File

@@ -0,0 +1,69 @@
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/utils/storage.dart';
class SetSwitchItem extends StatefulWidget {
final String? title;
final String? subTitle;
final String? setKey;
final bool? defaultVal;
const SetSwitchItem({
this.title,
this.subTitle,
this.setKey,
this.defaultVal,
Key? key,
}) : super(key: key);
@override
State<SetSwitchItem> createState() => _SetSwitchItemState();
}
class _SetSwitchItemState extends State<SetSwitchItem> {
// ignore: non_constant_identifier_names
Box Setting = GStrorage.setting;
late bool val;
@override
void initState() {
super.initState();
val = Setting.get(widget.setKey, defaultValue: widget.defaultVal ?? false);
}
@override
Widget build(BuildContext context) {
TextStyle titleStyle = Theme.of(context).textTheme.titleMedium!;
TextStyle subTitleStyle = Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline);
return ListTile(
enableFeedback: true,
onTap: () {
Setting.put(widget.setKey, !val);
},
title: Text(widget.title!, style: titleStyle),
subtitle: widget.subTitle != null
? Text(widget.subTitle!, style: subTitleStyle)
: null,
trailing: Transform.scale(
scale: 0.8,
child: Switch(
thumbIcon: MaterialStateProperty.resolveWith<Icon?>(
(Set<MaterialState> states) {
if (states.isNotEmpty && states.first == MaterialState.selected) {
return const Icon(Icons.done);
}
return null; // All other states will use the default thumbIcon.
}),
value: val,
onChanged: (value) {
val = value;
Setting.put(widget.setKey, value);
setState(() {});
}),
),
);
}
}

View File

@@ -13,36 +13,47 @@ import 'package:pilipala/models/video/reply/item.dart';
import 'package:pilipala/pages/video/detail/replyReply/index.dart';
import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:pilipala/utils/utils.dart';
class VideoDetailController extends GetxController
with GetSingleTickerProviderStateMixin {
int tabInitialIndex = 0;
TabController? tabCtr;
// tabs
RxList<String> tabs = <String>['简介', '评论'].obs;
// 视频aid
/// 路由传参
String bvid = Get.parameters['bvid']!;
int cid = int.parse(Get.parameters['cid']!);
// 视频类型 默认投稿视频
SearchType videoType = SearchType.video;
late PlayUrlModel data;
// 当前画质
late VideoQuality currentVideoQa;
// 当前音质
late AudioQuality currentAudioQa;
// 是否预渲染 骨架屏
bool preRender = false;
// 视频详情 上个页面传入
String heroTag = Get.arguments['heroTag'];
// 视频详情
Map videoItem = {};
// 视频类型 默认投稿视频
SearchType videoType = Get.arguments['videoType'] ?? SearchType.video;
/// tabs相关配置
int tabInitialIndex = 0;
late TabController tabCtr;
RxList<String> tabs = <String>['简介', '评论'].obs;
// 请求返回的视频信息
late PlayUrlModel data;
// 请求状态
RxBool isLoading = false.obs;
String heroTag = '';
/// 播放器配置 画质 音质 解码格式
late VideoQuality currentVideoQa;
late AudioQuality currentAudioQa;
late VideoDecodeFormats currentDecodeFormats;
// PlPlayerController plPlayerController = PlPlayerController();
// 是否开始自动播放 存在多p的情况下第二p需要为true
RxBool autoPlay = true.obs;
// 视频资源是否有效
RxBool isEffective = true.obs;
// 封面图的展示
RxBool isShowCover = true.obs;
// 硬解
RxBool enableHA = true.obs;
/// 本地存储
Box user = GStrorage.user;
Box localCache = GStrorage.localCache;
Box setting = GStrorage.setting;
int oid = 0;
// 评论id 请求楼中楼评论使用
@@ -50,39 +61,41 @@ class VideoDetailController extends GetxController
ReplyItemModel? firstFloor;
final scaffoldKey = GlobalKey<ScaffoldState>();
Timer? timer;
RxString bgCover = ''.obs;
Box user = GStrorage.user;
Box localCache = GStrorage.localCache;
PlPlayerController plPlayerController = PlPlayerController();
// 是否开始自动播放 存在多p的情况下第二p需要为true
RxBool autoPlay = true.obs;
// 视频资源是否有效
RxBool isEffective = true.obs;
// 封面图的展示
RxBool isShowCover = true.obs;
PlPlayerController plPlayerController = PlPlayerController.getInstance();
late VideoItem firstVideo;
late String videoUrl;
late String audioUrl;
late Duration defaultST;
// 默认记录历史记录
bool enableHeart = true;
@override
void onInit() {
super.onInit();
if (Get.arguments.isNotEmpty) {
if (Get.arguments.containsKey('videoItem')) {
preRender = true;
var args = Get.arguments['videoItem'];
Map argMap = Get.arguments;
var keys = argMap.keys.toList();
if (keys.isNotEmpty) {
if (keys.contains('videoItem')) {
var args = argMap['videoItem'];
if (args.pic != null && args.pic != '') {
videoItem['pic'] = args.pic;
bgCover.value = args.pic;
}
}
if (Get.arguments.containsKey('pic')) {
videoItem['pic'] = Get.arguments['pic'];
bgCover.value = Get.arguments['pic'];
if (keys.contains('pic')) {
videoItem['pic'] = argMap['pic'];
}
heroTag = Get.arguments['heroTag'];
videoType = Get.arguments['videoType'] ?? SearchType.video;
}
tabCtr = TabController(length: 2, vsync: this);
// queryVideoUrl();
autoPlay.value =
setting.get(SettingBoxKey.autoPlayEnable, defaultValue: true);
enableHA.value = setting.get(SettingBoxKey.enableHA, defaultValue: true);
if (user.get(UserBoxKey.userMid) == null ||
localCache.get(LocalCacheKey.historyPause) == true) {
enableHeart = false;
}
}
showReplyReplyPanel() {
@@ -107,7 +120,7 @@ class VideoDetailController extends GetxController
/// 更新画质、音质
/// TODO 继续进度播放
updatePlayer() {
Duration position = plPlayerController.position.value;
defaultST = plPlayerController.position.value;
plPlayerController.removeListeners();
plPlayerController.isBuffering.value = false;
plPlayerController.buffered.value = Duration.zero;
@@ -115,24 +128,30 @@ class VideoDetailController extends GetxController
/// 暂不匹配解码规则
/// 根据currentVideoQa 重新设置videoUrl
VideoItem firstVideo =
data.dash!.video!.firstWhere((i) => i.id == currentVideoQa.code);
// String videoUrl = firstVideo.baseUrl!;
// firstVideo =
// data.dash!.video!.firstWhere((i) => i.id == currentVideoQa.code);
// videoUrl = firstVideo.baseUrl!;
/// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl
List<VideoItem> videoList =
data.dash!.video!.where((i) => i.id == currentVideoQa.code).toList();
firstVideo = videoList
.firstWhere((i) => i.codecs!.startsWith(currentDecodeFormats.code));
videoUrl = firstVideo.baseUrl!;
/// 根据currentAudioQa 重新设置audioUrl
AudioItem firstAudio =
data.dash!.audio!.firstWhere((i) => i.id == currentAudioQa.code);
String audioUrl = firstAudio.baseUrl ?? '';
audioUrl = firstAudio.baseUrl ?? '';
playerInit(firstVideo, audioUrl, defaultST: position);
playerInit();
}
Future playerInit(firstVideo, audioSource,
{Duration defaultST = Duration.zero, int duration = 0}) async {
Future playerInit({video, audio, seekToTime, duration}) async {
await plPlayerController.setDataSource(
DataSource(
videoSource: firstVideo.baseUrl,
audioSource: audioSource,
videoSource: video ?? videoUrl,
audioSource: audio ?? audioUrl,
type: DataSourceType.network,
httpHeaders: {
'user-agent':
@@ -141,13 +160,19 @@ class VideoDetailController extends GetxController
},
),
// 硬解
enableHA: true,
enableHA: enableHA.value,
autoplay: autoPlay.value,
seekTo: defaultST,
duration: Duration(milliseconds: duration),
seekTo: seekToTime ?? defaultST,
duration: duration ?? Duration(milliseconds: data.timeLength ?? 0),
// 宽>高 水平 否则 垂直
direction:
firstVideo.width - firstVideo.height > 0 ? 'horizontal' : 'vertical',
direction: (firstVideo.width! - firstVideo.height!) > 0
? 'horizontal'
: 'vertical',
// 默认1倍速
speed: 1.0,
bvid: bvid,
cid: cid,
enableHeart: enableHeart,
);
}
@@ -163,58 +188,90 @@ class VideoDetailController extends GetxController
data = result['data'];
/// 优先顺序 省流模式 -> 设置中指定质量 -> 当前可选的最高质量
VideoItem firstVideo = data.dash!.video!.first;
// String videoUrl = firstVideo.baseUrl!;
//
currentVideoQa = VideoQualityCode.fromCode(firstVideo.id!)!;
// firstVideo = data.dash!.video!.first;
// videoUrl = firstVideo.baseUrl!;
// //
// currentVideoQa = VideoQualityCode.fromCode(firstVideo.id!)!;
// /// 优先顺序 设置中指定质量 -> 当前可选的最高质量
// AudioItem firstAudio =
// data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first : AudioItem();
// audioUrl = firstAudio.baseUrl ?? '';
List<VideoItem> allVideosList = data.dash!.video!;
try {
// 当前可播放的最高质量视频
int currentHighVideoQa = allVideosList.first.quality!.code;
//
int cacheVideoQa = setting.get(SettingBoxKey.defaultVideoQa,
defaultValue: currentHighVideoQa);
int resVideoQa = currentHighVideoQa;
if (cacheVideoQa <= currentHighVideoQa) {
List<int> numbers = data.acceptQuality!
.where((e) => e <= currentHighVideoQa)
.toList();
resVideoQa = Utils.findClosestNumber(cacheVideoQa, numbers);
}
currentVideoQa = VideoQualityCode.fromCode(resVideoQa)!;
/// 取出符合当前画质的videoList
List<VideoItem> videosList =
allVideosList.where((e) => e.quality!.code == resVideoQa).toList();
/// 优先顺序 设置中指定解码格式 -> 当前可选的首个解码格式
List<FormatItem> supportFormats = data.supportFormats!;
// 根据画质选编码格式
List supportDecodeFormats =
supportFormats.firstWhere((e) => e.quality == resVideoQa).codecs!;
try {
currentDecodeFormats = VideoDecodeFormatsCode.fromString(setting.get(
SettingBoxKey.defaultDecode,
defaultValue: supportDecodeFormats.first))!;
} catch (_) {}
/// 取出符合当前解码格式的videoItem
firstVideo = videosList
.firstWhere((e) => e.codecs!.startsWith(currentDecodeFormats.code));
videoUrl = firstVideo.baseUrl!;
} catch (_) {}
/// 优先顺序 设置中指定质量 -> 当前可选的最高质量
AudioItem firstAudio =
data.dash!.audio!.isNotEmpty ? data.dash!.audio!.first : AudioItem();
String audioUrl = firstAudio.baseUrl ?? '';
late AudioItem firstAudio;
List audiosList = data.dash!.audio!;
try {
if (audiosList.isNotEmpty) {
firstAudio = audiosList.first;
int resultAudioQa = setting.get(SettingBoxKey.defaultAudioQa,
defaultValue: firstAudio.id);
// 选择最接近的那个音轨
firstAudio = audiosList.firstWhere(
(e) => e.id == resultAudioQa,
orElse: () => AudioItem(),
);
} else {
firstAudio = AudioItem();
}
} catch (_) {}
audioUrl = firstAudio.baseUrl ?? '';
//
if (firstAudio.id != null) {
currentAudioQa = AudioQualityCode.fromCode(firstAudio.id!)!;
}
await playerInit(
firstVideo,
audioUrl,
defaultST: Duration(milliseconds: data.lastPlayTime!),
duration: data.timeLength ?? 0,
);
defaultST = Duration(milliseconds: data.lastPlayTime!);
await playerInit();
// await playerInit(
// firstVideo,
// audioUrl,
// defaultST: Duration(milliseconds: data.lastPlayTime!),
// duration: data.timeLength ?? 0,
// );
} else {
SmartDialog.showToast(result['msg'].toString());
}
return result;
}
void loopHeartBeat() {
timer = Timer.periodic(const Duration(seconds: 5), (timer) {
markHeartBeat();
});
}
void markHeartBeat() async {
if (user.get(UserBoxKey.userMid) == null) {
return;
}
if (localCache.get(LocalCacheKey.historyStatus) == true) {
return;
}
Duration progress = plPlayerController.position.value;
await VideoHttp.heartBeat(
bvid: bvid,
cid: cid,
progress: progress.inSeconds,
);
}
@override
void onClose() {
markHeartBeat();
if (timer != null && timer!.isActive) {
timer!.cancel();
}
super.onClose();
}
}

View File

@@ -51,6 +51,8 @@ class VideoIntroController extends GetxController {
RxMap followStatus = {}.obs;
int _tempThemeValue = -1;
RxInt lastPlayCid = 0.obs;
@override
void onInit() {
super.onInit();
@@ -76,6 +78,7 @@ class VideoIntroController extends GetxController {
}
}
userLogin = user.get(UserBoxKey.userLogin) != null;
lastPlayCid.value = int.parse(Get.parameters['cid']!);
}
// 获取视频简介&分p
@@ -83,6 +86,9 @@ class VideoIntroController extends GetxController {
var result = await VideoHttp.videoIntro(bvid: bvid);
if (result['status']) {
videoDetail.value = result['data']!;
if (videoDetail.value.pages!.isNotEmpty && lastPlayCid.value == 0) {
lastPlayCid.value = videoDetail.value.pages!.first.cid!;
}
Get.find<VideoDetailController>(tag: Get.arguments['heroTag'])
.tabs
.value = ['简介', '评论 ${result['data']!.stat!.reply}'];
@@ -408,6 +414,7 @@ class VideoIntroController extends GetxController {
videoReplyCtr.queryReplyList(type: 'init');
} catch (_) {}
this.bvid = bvid;
lastPlayCid.value = cid;
await queryVideoIntro();
}
}

View File

@@ -1,4 +1,3 @@
import 'package:flutter/gestures.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
@@ -20,6 +19,7 @@ import 'widgets/action_item.dart';
import 'widgets/action_row_item.dart';
import 'widgets/fav_panel.dart';
import 'widgets/intro_detail.dart';
import 'widgets/page.dart';
import 'widgets/season.dart';
class VideoIntroPanel extends StatefulWidget {
@@ -62,7 +62,6 @@ class _VideoIntroPanelState extends State<VideoIntroPanel>
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data['status']) {
// 请求成功
// return _buildView(context, false, videoDetail);
return Obx(
() => VideoInfo(
loadingStatus: false,
@@ -95,22 +94,35 @@ class VideoInfo extends StatefulWidget {
}
class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Map videoItem = Get.put(VideoIntroController()).videoItem!;
final VideoIntroController videoIntroController =
Get.put(VideoIntroController(), tag: Get.arguments['heroTag']);
bool isExpand = false;
final String heroTag = Get.arguments['heroTag'];
late final VideoIntroController videoIntroController;
late final VideoDetailController videoDetailCtr;
late final Map<dynamic, dynamic> videoItem;
late VideoDetailController? videoDetailCtr;
Box localCache = GStrorage.localCache;
late double sheetHeight;
late final bool loadingStatus; // 加载状态
late final dynamic owner;
late final dynamic follower;
late final dynamic followStatus;
@override
void initState() {
super.initState();
videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
videoItem = videoIntroController.videoItem!;
sheetHeight = localCache.get('sheetHeight');
loadingStatus = widget.loadingStatus;
owner = loadingStatus ? videoItem['owner'] : widget.videoDetail!.owner;
follower = loadingStatus
? '-'
: Utils.numFormat(videoIntroController.userStat['follower']);
followStatus = videoIntroController.followStatus;
}
// 收藏
@@ -141,24 +153,39 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
);
}
// 用户主页
onPushMember() {
feedBack();
int mid = !loadingStatus
? widget.videoDetail!.owner!.mid
: videoItem['owner'].mid;
String face = !loadingStatus
? widget.videoDetail!.owner!.face
: videoItem['owner'].face;
Get.toNamed('/member?mid=$mid',
arguments: {'face': face, 'heroTag': (mid + 99).toString()});
}
@override
Widget build(BuildContext context) {
ThemeData t = Theme.of(context);
Color outline = t.colorScheme.outline;
return SliverPadding(
padding: const EdgeInsets.only(
left: StyleString.safeSpace, right: StyleString.safeSpace, top: 10),
sliver: SliverToBoxAdapter(
child: !widget.loadingStatus || videoItem.isNotEmpty
child: !loadingStatus || videoItem.isNotEmpty
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(),
child: Row(
children: [
Expanded(
child: Text(
!widget.loadingStatus
!loadingStatus
? widget.videoDetail!.title
: videoItem['title'],
style: const TextStyle(
@@ -182,14 +209,18 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
return t.highlightColor.withOpacity(0.2);
}),
),
onPressed: () => showIntroDetail(),
icon: const Icon(Icons.more_horiz),
onPressed: showIntroDetail,
icon: Icon(
Icons.more_horiz,
color: Theme.of(context).colorScheme.primary,
),
),
),
],
),
),
InkWell(
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: () => showIntroDetail(),
child: Row(
children: [
@@ -237,107 +268,100 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
// 点赞收藏转发 布局样式2
// actionGrid(context, videoIntroController),
// 合集
if (!widget.loadingStatus &&
if (!loadingStatus &&
widget.videoDetail!.ugcSeason != null) ...[
SeasonPanel(
ugcSeason: widget.videoDetail!.ugcSeason!,
cid: widget.videoDetail!.pages!.first.cid,
sheetHeight: sheetHeight,
changeFuc: (bvid, cid, aid) => videoIntroController
.changeSeasonOrbangu(bvid, cid, aid),
Obx(
() => SeasonPanel(
ugcSeason: widget.videoDetail!.ugcSeason!,
cid: videoIntroController.lastPlayCid.value != 0
? videoIntroController.lastPlayCid.value
: widget.videoDetail!.pages!.first.cid,
sheetHeight: sheetHeight,
changeFuc: (bvid, cid, aid) => videoIntroController
.changeSeasonOrbangu(bvid, cid, aid),
),
)
],
if (!loadingStatus &&
widget.videoDetail!.pages != null &&
widget.videoDetail!.pages!.length > 1) ...[
Obx(() => PagesPanel(
pages: widget.videoDetail!.pages!,
cid: videoIntroController.lastPlayCid.value,
sheetHeight: sheetHeight,
changeFuc: (cid) =>
videoIntroController.changeSeasonOrbangu(
videoIntroController.bvid, cid, null),
))
],
GestureDetector(
onTap: () {
feedBack();
int mid = !widget.loadingStatus
? widget.videoDetail!.owner!.mid
: videoItem['owner'].mid;
String face = !widget.loadingStatus
? widget.videoDetail!.owner!.face
: videoItem['owner'].face;
Get.toNamed('/member?mid=$mid', arguments: {
'face': face,
'heroTag': (mid + 99).toString()
});
},
child: Padding(
padding: const EdgeInsets.only(
top: 12, bottom: 12, left: 4, right: 4),
onTap: onPushMember,
child: Container(
padding: const EdgeInsets.symmetric(
vertical: 12, horizontal: 4),
child: Row(
children: [
NetworkImgLayer(
type: 'avatar',
src: !widget.loadingStatus
? widget.videoDetail!.owner!.face
: videoItem['owner'].face,
src: loadingStatus
? owner.face
: widget.videoDetail!.owner!.face,
width: 34,
height: 34,
fadeInDuration: Duration.zero,
fadeOutDuration: Duration.zero,
),
const SizedBox(width: 10),
Text(
!widget.loadingStatus
? widget.videoDetail!.owner!.name
: videoItem['owner'].name,
style: const TextStyle(fontSize: 13),
),
Text(owner.name,
style: const TextStyle(fontSize: 13)),
const SizedBox(width: 6),
Text(
widget.loadingStatus
? '-'
: Utils.numFormat(
videoIntroController.userStat['follower']),
follower,
style: TextStyle(
fontSize: t.textTheme.labelSmall!.fontSize,
color: t.colorScheme.outline),
fontSize: t.textTheme.labelSmall!.fontSize,
color: outline,
),
),
const Spacer(),
AnimatedOpacity(
opacity: widget.loadingStatus ? 0 : 1,
opacity: loadingStatus ? 0 : 1,
duration: const Duration(milliseconds: 150),
child: SizedBox(
height: 32,
child: Obx(
() => videoIntroController
.followStatus.isNotEmpty
? TextButton(
onPressed: () => videoIntroController
.actionRelationMod(),
style: TextButton.styleFrom(
padding: const EdgeInsets.only(
left: 8, right: 8),
foregroundColor:
videoIntroController.followStatus[
'attribute'] !=
0
? t.colorScheme.outline
: t.colorScheme.onPrimary,
backgroundColor: videoIntroController
.followStatus[
'attribute'] !=
0
? t.colorScheme.onInverseSurface
: t.colorScheme
.primary, // 设置按钮背景色
),
child: Text(
videoIntroController.followStatus[
'attribute'] !=
0
? '已关注'
: '关注',
style: TextStyle(
fontSize: t.textTheme.labelMedium!
.fontSize),
),
)
: ElevatedButton(
onPressed: () => videoIntroController
.actionRelationMod(),
child: const Text('关注'),
),
() =>
videoIntroController.followStatus.isNotEmpty
? TextButton(
onPressed: videoIntroController
.actionRelationMod,
style: TextButton.styleFrom(
padding: const EdgeInsets.only(
left: 8, right: 8),
foregroundColor:
followStatus['attribute'] != 0
? outline
: t.colorScheme.onPrimary,
backgroundColor:
followStatus['attribute'] != 0
? t.colorScheme
.onInverseSurface
: t.colorScheme
.primary, // 设置按钮背景色
),
child: Text(
followStatus['attribute'] != 0
? '已关注'
: '关注',
style: TextStyle(
fontSize: t.textTheme
.labelMedium!.fontSize),
),
)
: ElevatedButton(
onPressed: videoIntroController
.actionRelationMod,
child: const Text('关注'),
),
),
),
),
@@ -359,66 +383,64 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Widget actionGrid(BuildContext context, videoIntroController) {
return LayoutBuilder(builder: (context, constraints) {
return Padding(
return Container(
padding: const EdgeInsets.only(top: 6, bottom: 10),
child: SizedBox(
height: constraints.maxWidth / 5 * 0.8,
child: GridView.count(
primary: false,
padding: const EdgeInsets.all(0),
crossAxisCount: 5,
childAspectRatio: 1.25,
children: <Widget>[
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.clock),
onTap: () => videoIntroController.actionShareVideo(),
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.star),
selectIcon: const Icon(FontAwesomeIcons.solidStar),
// onTap: () => videoIntroController.actionFavVideo(),
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()
height: constraints.maxWidth / 5 * 0.8,
child: GridView.count(
primary: false,
padding: const EdgeInsets.all(0),
crossAxisCount: 5,
childAspectRatio: 1.25,
children: <Widget>[
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.thumbsUp),
selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp),
onTap: () => videoIntroController.actionLikeVideo(),
selectStatus: videoIntroController.hasLike.value,
loadingStatus: loadingStatus,
text: !loadingStatus
? widget.videoDetail!.stat!.like!.toString()
: '-'),
],
),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.clock),
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: loadingStatus,
text: '稍后再看'),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.b),
selectIcon: const Icon(FontAwesomeIcons.b),
onTap: () => videoIntroController.actionCoinVideo(),
selectStatus: videoIntroController.hasCoin.value,
loadingStatus: loadingStatus,
text: !loadingStatus
? widget.videoDetail!.stat!.coin!.toString()
: '-'),
),
Obx(
() => ActionItem(
icon: const Icon(FontAwesomeIcons.star),
selectIcon: const Icon(FontAwesomeIcons.solidStar),
// onTap: () => videoIntroController.actionFavVideo(),
onTap: () => showFavBottomSheet(),
selectStatus: videoIntroController.hasFav.value,
loadingStatus: loadingStatus,
text: !loadingStatus
? widget.videoDetail!.stat!.favorite!.toString()
: '-'),
),
ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare),
onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false,
loadingStatus: loadingStatus,
text: !loadingStatus
? widget.videoDetail!.stat!.share!.toString()
: '-'),
],
),
);
});
@@ -431,10 +453,9 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
icon: const Icon(FontAwesomeIcons.thumbsUp),
onTap: () => videoIntroController.actionLikeVideo(),
selectStatus: videoIntroController.hasLike.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.like!.toString()
: '-',
loadingStatus: loadingStatus,
text:
!loadingStatus ? widget.videoDetail!.stat!.like!.toString() : '-',
),
),
const SizedBox(width: 8),
@@ -443,10 +464,9 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
icon: const Icon(FontAwesomeIcons.b),
onTap: () => videoIntroController.actionCoinVideo(),
selectStatus: videoIntroController.hasCoin.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.coin!.toString()
: '-',
loadingStatus: loadingStatus,
text:
!loadingStatus ? widget.videoDetail!.stat!.coin!.toString() : '-',
),
),
const SizedBox(width: 8),
@@ -455,8 +475,8 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
icon: const Icon(FontAwesomeIcons.heart),
onTap: () => showFavBottomSheet(),
selectStatus: videoIntroController.hasFav.value,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
loadingStatus: loadingStatus,
text: !loadingStatus
? widget.videoDetail!.stat!.favorite!.toString()
: '-',
),
@@ -468,57 +488,20 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
videoDetailCtr.tabCtr.animateTo(1);
},
selectStatus: false,
loadingStatus: widget.loadingStatus,
text: !widget.loadingStatus
? widget.videoDetail!.stat!.reply!.toString()
: '-',
loadingStatus: loadingStatus,
text:
!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
loadingStatus: loadingStatus,
// text: !loadingStatus
// ? widget.videoDetail!.stat!.share!.toString()
// : '-',
text: '转发'),
]);
}
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

@@ -33,24 +33,13 @@ class _FavPanelState extends State<FavPanel> {
child: Column(
children: [
AppBar(
toolbarHeight: 50,
automaticallyImplyLeading: false,
centerTitle: false,
elevation: 1,
title: Text(
'选择文件夹',
style: Theme.of(context).textTheme.titleMedium,
),
actions: [
TextButton(
onPressed: () async {
feedBack();
await widget.ctr!.actionFavVideo();
},
child: const Text('完成'),
),
const SizedBox(width: 6),
],
elevation: 0,
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.close_outlined)),
title:
Text('添加到收藏夹', style: Theme.of(context).textTheme.titleMedium),
),
Expanded(
child: Material(
@@ -63,45 +52,33 @@ class _FavPanelState extends State<FavPanel> {
return Obx(
() => ListView.builder(
itemCount:
widget.ctr!.favFolderData.value.list!.length + 1,
widget.ctr!.favFolderData.value.list!.length,
itemBuilder: (context, index) {
if (index == 0) {
return const SizedBox(height: 10);
} else {
return ListTile(
onTap: () => widget.ctr!.onChoose(
widget.ctr!.favFolderData.value
.list![index - 1].favState !=
1,
index - 1),
dense: true,
leading:
const Icon(Icons.folder_special_outlined),
minLeadingWidth: 0,
title: Text(widget.ctr!.favFolderData.value
.list![index - 1].title!),
subtitle: Text(
'${widget.ctr!.favFolderData.value.list![index - 1].mediaCount}个内容',
style: TextStyle(
color:
Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context)
.textTheme
.labelSmall!
.fontSize),
return ListTile(
onTap: () => widget.ctr!.onChoose(
widget.ctr!.favFolderData.value.list![index]
.favState !=
1,
index),
dense: true,
leading: const Icon(Icons.folder_outlined),
minLeadingWidth: 0,
title: Text(widget.ctr!.favFolderData.value
.list![index].title!),
subtitle: Text(
'${widget.ctr!.favFolderData.value.list![index].mediaCount}个内容',
),
trailing: Transform.scale(
scale: 0.9,
child: Checkbox(
value: widget.ctr!.favFolderData.value
.list![index].favState ==
1,
onChanged: (bool? checkValue) =>
widget.ctr!.onChoose(checkValue!, index),
),
trailing: Transform.scale(
scale: 0.9,
child: Checkbox(
value: widget.ctr!.favFolderData.value
.list![index - 1].favState ==
1,
onChanged: (bool? checkValue) => widget.ctr!
.onChoose(checkValue!, index - 1),
),
),
);
}
),
);
},
),
);
@@ -119,6 +96,47 @@ class _FavPanelState extends State<FavPanel> {
),
),
),
Divider(
height: 1,
color: Theme.of(context).disabledColor.withOpacity(0.08),
),
Padding(
padding: EdgeInsets.only(
left: 20,
right: 20,
top: 12,
bottom: MediaQuery.of(context).padding.bottom + 12,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => Get.back(),
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 30, right: 30),
backgroundColor: Theme.of(context)
.colorScheme
.onInverseSurface, // 设置按钮背景色
),
child: const Text('取消'),
),
const SizedBox(width: 10),
TextButton(
onPressed: () async {
feedBack();
await widget.ctr!.actionFavVideo();
},
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 30, right: 30),
foregroundColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor:
Theme.of(context).colorScheme.primary, // 设置按钮背景色
),
child: const Text('完成'),
),
],
),
),
],
),
);

View File

@@ -27,19 +27,20 @@ class IntroDetail extends StatelessWidget {
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))),
InkWell(
onTap: () => Get.back(),
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.primary,
borderRadius:
const BorderRadius.all(Radius.circular(3))),
),
),
),
),
@@ -120,38 +121,33 @@ class IntroDetail extends StatelessWidget {
}
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},
);
},
),
List<TextSpan> spanChilds = List.generate(descV2.length, (index) {
final currentDesc = descV2[index];
switch (currentDesc.type) {
case 1:
return TextSpan(text: currentDesc.rawText);
case 2:
final colorSchemePrimary = Theme.of(context).colorScheme.primary;
final heroTag = Utils.makeHeroTag(currentDesc.bizId);
return TextSpan(
text: '@${currentDesc.rawText}',
style: TextStyle(color: colorSchemePrimary),
recognizer: TapGestureRecognizer()
..onTap = () {
Get.toNamed(
'/member?mid=${currentDesc.bizId}',
arguments: {'face': '', 'heroTag': heroTag},
);
},
);
}
default:
return const TextSpan();
}
} else {
spanChilds.add(TextSpan(text: desc));
}
});
return TextSpan(children: spanChilds);
}
}

View File

@@ -0,0 +1,203 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/models/video_detail_res.dart';
class PagesPanel extends StatefulWidget {
final List<Part> pages;
final int? cid;
final double? sheetHeight;
final Function? changeFuc;
const PagesPanel({
super.key,
required this.pages,
this.cid,
this.sheetHeight,
this.changeFuc,
});
@override
State<PagesPanel> createState() => _PagesPanelState();
}
class _PagesPanelState extends State<PagesPanel> {
late List<Part> episodes;
late int currentIndex;
@override
void initState() {
super.initState();
episodes = widget.pages;
currentIndex = episodes.indexWhere((e) => e.cid == widget.cid);
}
void changeFucCall(item, i) async {
await widget.changeFuc!(
item.cid,
);
currentIndex = i;
setState(() {});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Padding(
padding: const EdgeInsets.only(top: 10, bottom: 2),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('视频选集 '),
Expanded(
child: Text(
' 正在播放:${widget.pages[currentIndex].pagePart}',
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 12,
color: Theme.of(context).colorScheme.outline,
),
),
),
const SizedBox(width: 10),
SizedBox(
height: 34,
child: TextButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
showBottomSheet(
context: context,
builder: (_) => Container(
height: widget.sheetHeight,
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
Container(
height: 45,
padding:
const EdgeInsets.only(left: 14, right: 14),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
'合集(${episodes.length}',
style:
Theme.of(context).textTheme.titleMedium,
),
IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.pop(context),
),
],
),
),
Divider(
height: 1,
color: Theme.of(context)
.dividerColor
.withOpacity(0.1),
),
Expanded(
child: Material(
child: ListView.builder(
itemCount: episodes.length,
itemBuilder: (context, index) {
return InkWell(
onTap: () {
changeFucCall(episodes[index], index);
Get.back();
},
child: Padding(
padding: const EdgeInsets.only(
top: 10,
bottom: 10,
left: 15,
right: 15),
child: Text(
episodes[index].pagePart!,
style: TextStyle(
color: index == currentIndex
? Theme.of(context)
.colorScheme
.primary
: Theme.of(context)
.colorScheme
.onSurface),
),
),
);
},
),
),
),
],
),
),
);
},
child: Text(
'${widget.pages.length}',
style: const TextStyle(fontSize: 13),
),
),
),
],
),
),
Container(
height: 35,
margin: const EdgeInsets.only(bottom: 8),
child: ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: widget.pages.length,
itemExtent: 150,
itemBuilder: ((context, i) {
return Container(
width: 150,
margin: const EdgeInsets.only(right: 10),
child: Material(
color: Theme.of(context).colorScheme.onInverseSurface,
borderRadius: BorderRadius.circular(6),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => changeFucCall(widget.pages[i], i),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 8, horizontal: 8),
child: Row(
children: [
if (i == currentIndex) ...[
Image.asset(
'assets/images/live.gif',
color: Theme.of(context).colorScheme.primary,
height: 12,
),
const SizedBox(width: 6)
],
Expanded(
child: Text(
widget.pages[i].pagePart!,
maxLines: 1,
style: TextStyle(
fontSize: 13,
color: i == currentIndex
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.onSurface),
overflow: TextOverflow.ellipsis,
))
],
),
),
),
),
);
}),
),
)
],
);
}
}

View File

@@ -38,6 +38,7 @@ class _SeasonPanelState extends State<SeasonPanel> {
item.cid,
item.aid,
);
currentIndex = i;
Get.back();
}

View File

@@ -6,71 +6,74 @@ import 'package:pilipala/common/widgets/overlay_pop.dart';
import 'package:pilipala/common/widgets/video_card_h.dart';
import './controller.dart';
class RelatedVideoPanel extends GetView<ReleatedController> {
class RelatedVideoPanel extends StatefulWidget {
const RelatedVideoPanel({super.key});
@override
State<RelatedVideoPanel> createState() => _RelatedVideoPanelState();
}
class _RelatedVideoPanelState extends State<RelatedVideoPanel> {
final ReleatedController _releatedController =
Get.put(ReleatedController(), tag: Get.arguments['heroTag']);
@override
Widget build(BuildContext context) {
return GetBuilder(
init: ReleatedController(),
id: Get.arguments['heroTag'],
builder: (context) {
return FutureBuilder(
future: ReleatedController().queryRelatedVideo(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data!['status']) {
// 请求成功
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
if (index == snapshot.data['data'].length) {
return SizedBox(
height: MediaQuery.of(context).padding.bottom);
} else {
return Material(
child: VideoCardH(
videoItem: snapshot.data['data'][index],
longPress: () {
try {
ReleatedController().popupDialog =
_createPopupDialog(
snapshot.data['data'][index]);
Overlay.of(context)
.insert(ReleatedController().popupDialog!);
} catch (_) {
return {};
}
},
longPressEnd: () {
ReleatedController().popupDialog?.remove();
},
),
);
}
}, childCount: snapshot.data['data'].length + 1));
} else {
// 请求错误
return const Center(
child: Text('出错了'),
);
}
return FutureBuilder(
future: _releatedController.queryRelatedVideo(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data!['status']) {
// 请求成功
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
if (index == snapshot.data['data'].length) {
return SizedBox(height: MediaQuery.of(context).padding.bottom);
} else {
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
}, childCount: 5),
return Material(
child: VideoCardH(
videoItem: snapshot.data['data'][index],
longPress: () {
try {
_releatedController.popupDialog =
_createPopupDialog(snapshot.data['data'][index]);
Overlay.of(context)
.insert(_releatedController.popupDialog!);
} catch (err) {
return {};
}
},
longPressEnd: () {
_releatedController.popupDialog?.remove();
},
),
);
}
},
}, childCount: snapshot.data['data'].length + 1));
} else {
// 请求错误
return const Center(
child: Text('出错了'),
);
}
} else {
// 骨架屏
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const VideoCardHSkeleton();
}, childCount: 5),
);
});
}
},
);
}
OverlayEntry _createPopupDialog(videoItem) {
return OverlayEntry(
builder: (context) => AnimatedDialog(
child: OverlayPop(videoItem: videoItem),
closeFn: _releatedController.popupDialog?.remove,
child: OverlayPop(
videoItem: videoItem,
closeFn: _releatedController.popupDialog?.remove),
),
);
}

View File

@@ -2,6 +2,7 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/badge.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/models/video/reply/item.dart';
@@ -139,7 +140,13 @@ class ReplyItem extends StatelessWidget {
height: 11,
),
const SizedBox(width: 6),
if (replyItem!.isUp!) const UpTag(),
if (replyItem!.isUp!)
const PBadge(
text: 'UP',
size: 'small',
stack: 'normal',
fs: 9,
),
],
),
Positioned(
@@ -208,8 +215,15 @@ class ReplyItem extends StatelessWidget {
children: [
if (replyItem!.isTop!)
const WidgetSpan(
alignment: PlaceholderAlignment.top,
child: UpTag(tagText: 'TOP')),
alignment: PlaceholderAlignment.top,
child: PBadge(
text: 'TOP',
size: 'small',
stack: 'normal',
type: 'line',
fs: 9,
),
),
buildContent(context, replyItem!, replyReply, null),
],
),
@@ -391,7 +405,13 @@ class ReplyItemRow extends StatelessWidget {
),
if (replies![i].isUp)
const WidgetSpan(
child: UpTag(),
alignment: PlaceholderAlignment.top,
child: PBadge(
text: 'UP',
size: 'small',
stack: 'normal',
fs: 9,
),
),
buildContent(
context, replies![i], replyReply, replyItem),
@@ -758,32 +778,3 @@ InlineSpan buildContent(
// spanChilds.add(TextSpan(text: matchMember));
return TextSpan(children: spanChilds);
}
class UpTag extends StatelessWidget {
final String? tagText;
const UpTag({super.key, this.tagText = 'UP'});
@override
Widget build(BuildContext context) {
Color primary = Theme.of(context).colorScheme.primary;
return Container(
width: 24,
height: 14,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(3),
color: tagText == 'UP' ? primary : null,
border: Border.all(color: primary)),
margin: const EdgeInsets.only(right: 4),
child: Center(
child: Text(
tagText!,
style: TextStyle(
fontSize: 9,
color: tagText == 'UP'
? Theme.of(context).colorScheme.onPrimary
: primary,
),
),
),
);
}
}

View File

@@ -118,7 +118,7 @@ class _VideoReplyNewDialogState extends State<VideoReplyNewDialog>
@override
Widget build(BuildContext context) {
return Container(
height: 400,
height: 500,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
borderRadius: const BorderRadius.only(

View File

@@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/reply.dart';
import 'package:pilipala/models/common/reply_type.dart';
import 'package:pilipala/models/video/reply/data.dart';
import 'package:pilipala/models/video/reply/item.dart';
class VideoReplyReplyController extends GetxController {

View File

@@ -1,11 +1,13 @@
import 'dart:async';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/common/widgets/sliver_header.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/pages/bangumi/introduction/index.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
@@ -49,25 +51,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
void initState() {
super.initState();
plPlayerController = videoDetailController.plPlayerController;
plPlayerController!.onPlayerStatusChanged.listen(
(PlayerStatus status) {
videoDetailController.markHeartBeat();
playerStatus = status;
if (status == PlayerStatus.playing) {
videoDetailController.isShowCover.value = false;
videoDetailController.loopHeartBeat();
} else {
videoDetailController.timer!.cancel();
// 播放完成停止 or 切换下一个
if (status == PlayerStatus.completed) {
// 当只有1p或多p未打开自动播放时播放完成还原进度条展示控制栏
plPlayerController!.seekTo(Duration.zero);
plPlayerController!.onLockControl(false);
plPlayerController!.videoPlayerController!.pause();
}
}
},
);
playerListener();
appbarStream = StreamController<double>();
@@ -82,6 +66,26 @@ class _VideoDetailPageState extends State<VideoDetailPage>
_futureBuilderFuture = videoDetailController.queryVideoUrl();
}
// 播放器状态监听
void playerListener() {
plPlayerController!.onPlayerStatusChanged.listen(
(PlayerStatus status) async {
playerStatus = status;
if (status == PlayerStatus.playing) {
videoDetailController.isShowCover.value = false;
} else {
// 播放完成停止 or 切换下一个
if (status == PlayerStatus.completed) {
// 当只有1p或多p未打开自动播放时播放完成还原进度条展示控制栏
plPlayerController!.seekTo(Duration.zero);
plPlayerController!.onLockControl(false);
plPlayerController!.videoPlayerController!.pause();
}
}
},
);
}
// 继续播放或重新播放
void continuePlay() async {
await _extendNestCtr.animateTo(0,
@@ -91,19 +95,15 @@ class _VideoDetailPageState extends State<VideoDetailPage>
@override
void dispose() {
plPlayerController!.pause();
plPlayerController!.dispose();
if (videoDetailController.timer != null) {
videoDetailController.timer!.cancel();
}
super.dispose();
}
@override
// 离开当前页面时
void didPushNext() async {
if (videoDetailController.timer!.isActive) {
videoDetailController.timer!.cancel();
}
videoDetailController.defaultST = plPlayerController!.position.value;
plPlayerController!.pause();
super.didPushNext();
}
@@ -111,13 +111,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
@override
// 返回当前页面时
void didPopNext() async {
videoDetailController.playerInit();
if (_extendNestCtr.position.pixels == 0) {
await Future.delayed(const Duration(milliseconds: 300));
plPlayerController!.play();
}
if (!videoDetailController.timer!.isActive) {
videoDetailController.loopHeartBeat();
}
super.didPopNext();
}
@@ -156,8 +154,11 @@ class _VideoDetailPageState extends State<VideoDetailPage>
scrolledUnderElevation: 0,
forceElevated: innerBoxIsScrolled,
expandedHeight: videoHeight,
// backgroundColor: Colors.transparent,
backgroundColor: Theme.of(context).colorScheme.background,
backgroundColor:
MediaQuery.of(Get.context!).platformBrightness ==
Brightness.dark
? Colors.black
: Theme.of(context).colorScheme.background,
flexibleSpace: FlexibleSpaceBar(
background: Padding(
padding: EdgeInsets.only(top: statusBarHeight),
@@ -226,10 +227,17 @@ class _VideoDetailPageState extends State<VideoDetailPage>
backgroundColor:
Colors.transparent,
actions: [
/// TODO
IconButton(
tooltip: '稍后再看',
onPressed: () {},
onPressed: () async {
var res = await UserHttp
.toViewLater(
bvid:
videoDetailController
.bvid);
SmartDialog.showToast(
res['msg']);
},
icon: const Icon(Icons
.history_outlined))
],
@@ -284,39 +292,20 @@ class _VideoDetailPageState extends State<VideoDetailPage>
children: [
Opacity(
opacity: 0,
child: Container(
child: SizedBox(
width: double.infinity,
height: 0,
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: Theme.of(context)
.dividerColor
.withOpacity(0.1),
),
child: Obx(
() => TabBar(
controller: videoDetailController.tabCtr,
dividerColor: Colors.transparent,
indicatorColor:
Theme.of(context).colorScheme.background,
tabs: videoDetailController.tabs
.map((String name) => Tab(text: name))
.toList(),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.max,
children: [
Container(
width: 280,
margin: const EdgeInsets.only(left: 20),
child: Obx(
() => TabBar(
controller: videoDetailController.tabCtr,
dividerColor: Colors.transparent,
indicatorColor:
Theme.of(context).colorScheme.background,
tabs: videoDetailController.tabs
.map((String name) => Tab(text: name))
.toList(),
),
),
),
],
),
),
),
Expanded(
@@ -333,20 +322,30 @@ class _VideoDetailPageState extends State<VideoDetailPage>
const VideoIntroPanel(),
] else if (videoDetailController.videoType ==
SearchType.media_bangumi) ...[
const BangumiIntroPanel()
BangumiIntroPanel(
cid: videoDetailController.cid)
],
if (videoDetailController.videoType ==
SearchType.video) ...[
SliverPersistentHeader(
floating: true,
pinned: true,
delegate: SliverHeaderDelegate(
height: 50,
child:
const MenuRow(loadingStatus: false),
),
// if (videoDetailController.videoType ==
// SearchType.video) ...[
// SliverPersistentHeader(
// floating: true,
// pinned: true,
// delegate: SliverHeaderDelegate(
// height: 50,
// child:
// const MenuRow(loadingStatus: false),
// ),
// ),
// ],
SliverToBoxAdapter(
child: Divider(
indent: 12,
endIndent: 12,
color: Theme.of(context)
.dividerColor
.withOpacity(0.06),
),
],
),
const RelatedVideoPanel(),
],
);

View File

@@ -28,6 +28,7 @@ class _HeaderControlState extends State<HeaderControl> {
late PlayUrlModel videoInfo;
List<PlaySpeed> playSpeed = PlaySpeed.values;
TextStyle subTitleStyle = const TextStyle(fontSize: 12);
TextStyle titleStyle = const TextStyle(fontSize: 14);
Size get preferredSize => const Size(double.infinity, kToolbarHeight);
@override
@@ -81,7 +82,7 @@ class _HeaderControlState extends State<HeaderControl> {
enabled: false,
leading:
const Icon(Icons.network_cell_outlined, size: 20),
title: const Text('省流模式'),
title: Text('省流模式', style: titleStyle),
subtitle: Text('低画质 减少视频缓存', style: subTitleStyle),
trailing: Transform.scale(
scale: 0.75,
@@ -99,22 +100,22 @@ class _HeaderControlState extends State<HeaderControl> {
),
),
),
Obx(
() => ListTile(
onTap: () => {Get.back(), showSetSpeedSheet()},
dense: true,
leading: const Icon(Icons.speed_outlined, size: 20),
title: const Text('播放速度'),
subtitle: Text(
'当前倍速 x${widget.controller!.playbackSpeed}',
style: subTitleStyle),
),
),
// Obx(
// () => ListTile(
// onTap: () => {Get.back(), showSetSpeedSheet()},
// dense: true,
// leading: const Icon(Icons.speed_outlined, size: 20),
// title: Text('播放速度', style: titleStyle),
// subtitle: Text(
// '当前倍速 x${widget.controller!.playbackSpeed}',
// style: subTitleStyle),
// ),
// ),
ListTile(
onTap: () => {Get.back(), showSetVideoQa()},
dense: true,
leading: const Icon(Icons.play_circle_outline, size: 20),
title: const Text('选择画质'),
title: Text('选择画质', style: titleStyle),
subtitle: Text(
'当前画质 ${widget.videoDetailCtr!.currentVideoQa.description}',
style: subTitleStyle),
@@ -123,24 +124,33 @@ class _HeaderControlState extends State<HeaderControl> {
onTap: () => {Get.back(), showSetAudioQa()},
dense: true,
leading: const Icon(Icons.album_outlined, size: 20),
title: const Text('选择音质'),
title: Text('选择音质', style: titleStyle),
subtitle: Text(
'当前音质 ${widget.videoDetailCtr!.currentAudioQa.description}',
style: subTitleStyle),
),
ListTile(
onTap: () {},
onTap: () => {Get.back(), showSetDecodeFormats()},
dense: true,
enabled: false,
leading: const Icon(Icons.play_circle_outline, size: 20),
title: const Text('播放设置'),
leading: const Icon(Icons.av_timer_outlined, size: 20),
title: Text('解码格式', style: titleStyle),
subtitle: Text(
'当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}',
style: subTitleStyle),
),
// ListTile(
// onTap: () {},
// dense: true,
// enabled: false,
// leading: const Icon(Icons.play_circle_outline, size: 20),
// title: Text('播放设置', style: titleStyle),
// ),
ListTile(
onTap: () {},
dense: true,
enabled: false,
leading: const Icon(Icons.subtitles_outlined, size: 20),
title: const Text('弹幕设置'),
title: Text('弹幕设置', style: titleStyle),
),
],
),
@@ -250,7 +260,7 @@ class _HeaderControlState extends State<HeaderControl> {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('选择画质'),
Text('选择画质', style: titleStyle),
const SizedBox(width: 4),
Icon(
Icons.info_outline,
@@ -309,7 +319,6 @@ class _HeaderControlState extends State<HeaderControl> {
/// 选择音质
void showSetAudioQa() {
List<FormatItem> videoFormat = videoInfo.supportFormats!;
AudioQuality currentAudioQa = widget.videoDetailCtr!.currentAudioQa;
List<AudioItem> audio = videoInfo.dash!.audio!;
@@ -329,7 +338,9 @@ class _HeaderControlState extends State<HeaderControl> {
margin: const EdgeInsets.all(12),
child: Column(
children: [
const SizedBox(height: 45, child: Center(child: Text('选择音质'))),
SizedBox(
height: 45,
child: Center(child: Text('选择音质', style: titleStyle))),
Expanded(
child: Material(
child: ListView(
@@ -370,6 +381,74 @@ class _HeaderControlState extends State<HeaderControl> {
);
}
// 选择解码格式
void showSetDecodeFormats() {
// 当前选中的解码格式
VideoDecodeFormats currentDecodeFormats =
widget.videoDetailCtr!.currentDecodeFormats;
// 当前视频可用的解码格式
List<FormatItem> videoFormat = videoInfo.supportFormats!;
List list = videoFormat.first.codecs!;
showModalBottomSheet(
context: context,
elevation: 0,
backgroundColor: Colors.transparent,
builder: (BuildContext context) {
return Container(
width: double.infinity,
height: 250,
clipBehavior: Clip.hardEdge,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
margin: const EdgeInsets.all(12),
child: Column(
children: [
SizedBox(
height: 45,
child: Center(child: Text('选择解码格式', style: titleStyle))),
Expanded(
child: Material(
child: ListView(
children: [
for (var i in list) ...[
ListTile(
onTap: () {
widget.videoDetailCtr!.currentDecodeFormats =
VideoDecodeFormatsCode.fromString(i)!;
widget.videoDetailCtr!.updatePlayer();
Get.back();
},
dense: true,
contentPadding:
const EdgeInsets.only(left: 20, right: 20),
title: Text(VideoDecodeFormatsCode.fromString(i)!
.description!),
subtitle: Text(
i!,
style: subTitleStyle,
),
trailing: i.startsWith(currentDecodeFormats.code)
? Icon(
Icons.done,
color: Theme.of(context).colorScheme.primary,
)
: const SizedBox(),
),
]
],
),
),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
final _ = widget.controller!;
@@ -403,7 +482,11 @@ class _HeaderControlState extends State<HeaderControl> {
size: 15,
color: Colors.white,
),
fuc: () => Get.offAll(const MainApp()),
fuc: () {
// 销毁播放器实例
widget.controller!.dispose(type: 'all');
Get.offAll(const MainApp());
},
),
const Spacer(),
// ComBtn(

View File

@@ -1,17 +1,15 @@
// ignore_for_file: avoid_print
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/constants.dart';
import 'package:pilipala/http/init.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/pages/dynamics/index.dart';
import 'package:pilipala/pages/mine/index.dart';
import 'package:pilipala/pages/rcmd/controller.dart';
import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/utils/cookie.dart';
import 'package:pilipala/utils/event_bus.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:webview_cookie_manager/webview_cookie_manager.dart';
import 'package:webview_flutter/webview_flutter.dart';
class WebviewController extends GetxController {
@@ -21,6 +19,7 @@ class WebviewController extends GetxController {
final WebViewController controller = WebViewController();
RxInt loadProgress = 0.obs;
RxBool loadShow = true.obs;
EventBus eventBus = EventBus();
@override
void onInit() {
@@ -29,17 +28,17 @@ class WebviewController extends GetxController {
type = Get.parameters['type']!;
pageTitle = Get.parameters['pageTitle']!;
webviewInit();
if (type == 'login') {
controller.clearCache();
controller.clearLocalStorage();
WebViewCookieManager().clearCookies();
controller.setUserAgent(Request().headerUa('mob'));
}
webviewInit();
}
webviewInit() {
controller
..setUserAgent(Request().headerUa('mob'))
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
NavigationDelegate(
@@ -50,39 +49,53 @@ class WebviewController extends GetxController {
},
onPageStarted: (String url) {},
// 加载完成
onPageFinished: (String url) async {
onUrlChange: (UrlChange urlChange) async {
loadShow.value = false;
String url = urlChange.url ?? '';
if (type == 'login' &&
(url.startsWith(
'https://passport.bilibili.com/web/sso/exchange_cookie') ||
url.startsWith('https://m.bilibili.com/'))) {
try {
var cookies =
await WebviewCookieManager().getCookies(HttpString.baseUrl);
var apiCookies = await WebviewCookieManager()
.getCookies(HttpString.baseApiUrl);
var tCookies =
await WebviewCookieManager().getCookies(HttpString.tUrl);
await SetCookie.onSet(cookies, HttpString.baseUrl);
await SetCookie.onSet(apiCookies, HttpString.baseApiUrl);
await SetCookie.onSet(tCookies, HttpString.tUrl);
await UserHttp.userInfo();
await SetCookie.onSet();
var result = await UserHttp.userInfo();
UserHttp.thirdLogin();
print('网页登录: $result');
if (result['status'] && result['data'].isLogin) {
SmartDialog.showToast('登录成功');
Box user = GStrorage.user;
user.put(UserBoxKey.userLogin, true);
user.put(UserBoxKey.userName, result['data'].uname);
user.put(UserBoxKey.userFace, result['data'].face);
user.put(UserBoxKey.userMid, result['data'].mid);
Box userInfoCache = GStrorage.userInfo;
userInfoCache.put('userInfoCache', result['data']);
Get.find<MineController>().userInfo.value = result['data'];
Get.find<MineController>().onInit();
Get.find<RcmdController>().queryRcmdFeed('onRefresh');
Get.find<DynamicsController>().queryFollowDynamic();
try {
Box user = GStrorage.user;
user.put(UserBoxKey.userLogin, true);
user.put(UserBoxKey.userName, result['data'].uname);
user.put(UserBoxKey.userFace, result['data'].face);
user.put(UserBoxKey.userMid, result['data'].mid);
Box userInfoCache = GStrorage.userInfo;
userInfoCache.put('userInfoCache', result['data']);
// 通知更新
eventBus.emit(EventName.loginEvent, {'status': true});
HomeController homeCtr = Get.find<HomeController>();
homeCtr.updateLoginStatus(true);
} catch (err) {
SmartDialog.show(builder: (context) {
return AlertDialog(
title: const Text('登录遇到问题'),
content: Text(err.toString()),
actions: [
TextButton(
onPressed: () => controller.reload(),
child: const Text('确认'),
)
],
);
});
}
Get.back();
} else {
// 获取用户信息失败
SmartDialog.showToast(result.msg);
}
} catch (e) {
print(e);

View File

@@ -1,19 +1,22 @@
// ignore_for_file: avoid_print
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter/painting.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:flutter_volume_controller/flutter_volume_controller.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:pilipala/http/video.dart';
import 'package:pilipala/plugin/pl_player/models/data_source.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:universal_platform/universal_platform.dart';
import 'package:volume_controller/volume_controller.dart';
import 'package:wakelock_plus/wakelock_plus.dart';
// import 'package:wakelock_plus/wakelock_plus.dart';
import 'models/data_status.dart';
import 'models/play_speed.dart';
@@ -25,6 +28,9 @@ class PlPlayerController {
Player? _videoPlayerController;
VideoController? _videoController;
// 添加一个私有静态变量来保存实例
static PlPlayerController? _instance;
// 流事件 监听播放状态变化
StreamSubscription? _playerEventSubs;
@@ -34,7 +40,7 @@ class PlPlayerController {
///
final PlPlayerDataStatus dataStatus = PlPlayerDataStatus();
bool controlsEnabled = true;
// bool controlsEnabled = false;
/// 响应数据
// 播放位置
@@ -44,6 +50,8 @@ class PlPlayerController {
final Rx<Duration> _duration = Rx(Duration.zero);
final Rx<Duration> _buffered = Rx(Duration.zero);
final Rx<int> _playerCount = Rx(0);
final Rx<double> _playbackSpeed = 1.0.obs;
final Rx<double> _currentVolume = 1.0.obs;
final Rx<double> _currentBrightness = 0.0.obs;
@@ -55,18 +63,27 @@ class PlPlayerController {
final Rx<bool> _doubleSpeedStatus = false.obs;
final Rx<bool> _controlsLock = false.obs;
final Rx<bool> _isFullScreen = false.obs;
// 默认投稿视频格式
static Rx<String> _videoType = 'archive'.obs;
final Rx<String> _direction = 'horizontal'.obs;
Rx<bool> videoFitChanged = false.obs;
final Rx<BoxFit> _videoFit = Rx(BoxFit.fill);
final Rx<BoxFit> _videoFit = Rx(BoxFit.contain);
///
// ignore: prefer_final_fields
Rx<bool> _isSliderMoving = false.obs;
PlaylistMode _looping = PlaylistMode.none;
bool _autoPlay = false;
final bool _listenersInitialized = false;
// 记录历史记录
String _bvid = '';
int _cid = 0;
int _heartDuration = 0;
bool _enableHeart = true;
Timer? _timer;
Timer? _timerForSeek;
Timer? _timerForVolume;
@@ -77,13 +94,13 @@ class PlPlayerController {
// final Durations durations;
List<BoxFit> fits = [
BoxFit.contain,
BoxFit.cover,
BoxFit.fill,
BoxFit.fitHeight,
BoxFit.fitWidth,
BoxFit.scaleDown
List<Map<String, dynamic>> videoFitType = [
{'attr': BoxFit.contain, 'desc': '包含'},
{'attr': BoxFit.cover, 'desc': '覆盖'},
{'attr': BoxFit.fill, 'desc': '填充'},
{'attr': BoxFit.fitHeight, 'desc': '高度适应'},
{'attr': BoxFit.fitWidth, 'desc': '宽度适应'},
{'attr': BoxFit.scaleDown, 'desc': '缩小适应'},
];
PreferredSizeWidget? headerControl;
@@ -172,10 +189,27 @@ class PlPlayerController {
/// 全屏方向
Rx<String> get direction => _direction;
PlPlayerController({
// 直播间 传false 关闭控制栏
this.controlsEnabled = true,
this.fits = const [
Rx<int> get playerCount => _playerCount;
///
Rx<String> get videoType => _videoType;
// 添加一个私有构造函数
PlPlayerController._() {
_videoType = videoType;
// _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) {
// if (status == PlayerStatus.playing) {
// WakelockPlus.enable();
// } else {
// WakelockPlus.disable();
// }
// });
}
// 获取实例 传参
static PlPlayerController getInstance({
String videoType = 'archive',
List<BoxFit> fits = const [
BoxFit.contain,
BoxFit.cover,
BoxFit.fill,
@@ -184,14 +218,11 @@ class PlPlayerController {
BoxFit.scaleDown
],
}) {
controlsEnabled = controlsEnabled;
_playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) {
if (status == PlayerStatus.playing) {
WakelockPlus.enable();
} else {
WakelockPlus.enable();
}
});
// 如果实例尚未创建,则创建一个新实例
_instance ??= PlPlayerController._();
_instance!._playerCount.value += 1;
_videoType.value = videoType;
return _instance!;
}
// 初始化资源
@@ -211,19 +242,24 @@ class PlPlayerController {
Duration? duration,
// 方向
String? direction,
// 全屏模式
// 记录历史记录
String bvid = '',
int cid = 0,
// 历史记录开关
bool enableHeart = true,
}) async {
try {
_autoPlay = autoplay;
_looping = looping;
// 初始化视频时长
_duration.value = duration ?? Duration.zero;
// 初始化视频倍速
_playbackSpeed.value = speed;
// 初始化数据加载状态
dataStatus.status.value = DataStatus.loading;
// 初始化全屏方向
_direction.value = direction ?? 'horizontal';
_bvid = bvid;
_cid = cid;
_enableHeart = enableHeart;
if (_videoPlayerController != null &&
_videoPlayerController!.state.playing) {
@@ -234,7 +270,7 @@ class PlPlayerController {
_videoPlayerController = await _createVideoController(
dataSource, _looping, enableHA, width, height);
// 获取视频时长 00:00
_duration.value = _videoPlayerController!.state.duration;
_duration.value = duration ?? _videoPlayerController!.state.duration;
// 数据加载完成
dataStatus.status.value = DataStatus.loaded;
@@ -258,6 +294,13 @@ class PlPlayerController {
double? width,
double? height,
) async {
// 每次配置时先移除监听
removeListeners();
isBuffering.value = false;
buffered.value = Duration.zero;
_heartDuration = 0;
_position.value = Duration.zero;
Player player = _videoPlayerController ??
Player(
configuration: const PlayerConfiguration(
@@ -364,6 +407,7 @@ class PlPlayerController {
} else {
// playerStatus.status.value = PlayerStatus.paused;
}
makeHeartBeat(_position.value.inSeconds, type: 'status');
}),
videoPlayerController!.stream.completed.listen((event) {
if (event) {
@@ -371,12 +415,14 @@ class PlPlayerController {
} else {
// playerStatus.status.value = PlayerStatus.playing;
}
makeHeartBeat(_position.value.inSeconds, type: 'status');
}),
videoPlayerController!.stream.position.listen((event) {
_position.value = event;
if (!isSliderMoving.value) {
_sliderPosition.value = event;
}
makeHeartBeat(event.inSeconds);
}),
videoPlayerController!.stream.duration.listen((event) {
duration.value = event;
@@ -413,7 +459,7 @@ class PlPlayerController {
}
_position.value = position;
if (duration.value.inSeconds != 0) {
// await _videoPlayerController!.stream.buffer.first;
await _videoPlayerController!.stream.buffer.first;
await _videoPlayerController?.seek(position);
// if (playerStatus.stopped) {
// play();
@@ -521,7 +567,10 @@ class PlPlayerController {
/// 音量
Future<void> getCurrentVolume() async {
_currentVolume.value = await VolumeController().getVolume();
// mac try...catch
try {
_currentVolume.value = (await FlutterVolumeController.getVolume())!;
} catch (_) {}
}
Future<void> setVolume(double volumeNew,
@@ -537,7 +586,8 @@ class PlPlayerController {
volume.value = volumeNew;
try {
VolumeController().setVolume(volumeNew, showSystemUI: false);
FlutterVolumeController.showSystemUI = false;
await FlutterVolumeController.setVolume(volumeNew);
} catch (err) {
print(err);
}
@@ -583,10 +633,17 @@ class PlPlayerController {
void toggleVideoFit() {
videoFitChangedTimer?.cancel();
videoFitChanged.value = true;
if (fits.indexOf(_videoFit.value) < fits.length - 1) {
_videoFit.value = fits[fits.indexOf(_videoFit.value) + 1];
// 范围内
List attrs = videoFitType.map((e) => e['attr']).toList();
if (attrs.indexOf(_videoFit.value) < attrs.length - 1) {
int index = attrs.indexOf(_videoFit.value);
_videoFit.value = attrs[index + 1];
print(videoFitType[index + 1]['desc']);
SmartDialog.showToast(videoFitType[index + 1]['desc']);
} else {
_videoFit.value = fits[0];
// 默认 contain
_videoFit.value = videoFitType.first['attr'];
SmartDialog.showToast(videoFitType.first['desc']);
}
videoFitChangedTimer = Timer(const Duration(seconds: 1), () {
videoFitChangedTimer = null;
@@ -607,9 +664,10 @@ class PlPlayerController {
/// 读取fit
Future<void> getVideoFit() async {
String fitValue = videoStorage.get(VideoBoxKey.videoBrightness,
defaultValue: 'fitHeight');
_videoFit.value = fits.firstWhere((element) => element.name == fitValue);
String fitValue =
videoStorage.get(VideoBoxKey.videoBrightness, defaultValue: 'contain');
_videoFit.value = videoFitType
.firstWhere((element) => element['attr'] == fitValue)['attr'];
}
/// 缓存亮度
@@ -630,9 +688,18 @@ class PlPlayerController {
}
}
/// 设置长按倍速状态
/// 设置长按倍速状态 live模式下禁用
void setDoubleSpeedStatus(bool val) {
if (videoType.value == 'live') {
return;
}
_doubleSpeedStatus.value = val;
double currentSpeed = playbackSpeed;
if (val) {
setPlaybackSpeed(currentSpeed * 2);
} else {
setPlaybackSpeed(currentSpeed / 2);
}
}
/// 关闭控制栏
@@ -662,7 +729,41 @@ class PlPlayerController {
videoFitChangedTimer?.cancel();
}
Future<void> dispose() async {
// 记录播放记录
Future makeHeartBeat(progress, {type = 'playing'}) async {
if (!_enableHeart) {
return false;
}
// 播放状态变化时,更新
if (type == 'status') {
await VideoHttp.heartBeat(
bvid: _bvid,
cid: _cid,
progress:
playerStatus.status.value == PlayerStatus.completed ? -1 : progress,
);
} else
// 正常播放时间隔5秒更新一次
if (progress - _heartDuration >= 5) {
_heartDuration = progress;
await VideoHttp.heartBeat(
bvid: _bvid,
cid: _cid,
progress: progress,
);
}
}
Future<void> dispose({String type = 'single'}) async {
// 每次减1最后销毁
if (type == 'single') {
_playerCount.value -= 1;
_heartDuration = 0;
if (playerCount.value > 0) {
return;
}
}
_timer?.cancel();
_timerForVolume?.cancel();
_timerForGettingVolume?.cancel();
@@ -685,5 +786,6 @@ class PlPlayerController {
removeListeners();
await _videoPlayerController?.dispose();
_videoPlayerController = null;
_instance = null;
}
}

View File

@@ -7,3 +7,20 @@ enum FullScreenMode {
// 始终横屏
horizontal
}
extension FullScreenModeDesc on FullScreenMode {
String get description => ['自适应', '始终竖屏', '始终横屏'][index];
}
extension FullScreenModeCode on FullScreenMode {
static final List<int> _codeList = [0, 1, 2];
int get code => _codeList[index];
static FullScreenMode? fromCode(int code) {
final index = _codeList.indexOf(code);
if (index != -1) {
return FullScreenMode.values[index];
}
return null;
}
}

View File

@@ -1,16 +1,36 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
// import 'package:auto_orientation/auto_orientation.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
//横屏
/// 低版本xcode不支持auto_orientation
// Future<void> landScape() async {
// if (Platform.isAndroid || Platform.isIOS) {
// await AutoOrientation.landscapeAutoMode(forceSensor: true);
// }
// }
Future<void> landScape() async {
dynamic document;
try {
if (kIsWeb) {
await document.documentElement?.requestFullscreen();
} else if (Platform.isAndroid || Platform.isIOS) {
await SystemChrome.setEnabledSystemUIMode(
SystemUiMode.immersiveSticky,
overlays: [],
);
await SystemChrome.setPreferredOrientations(
[
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
],
);
} else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
await const MethodChannel('com.alexmercerind/media_kit_video')
.invokeMethod(
'Utils.EnterNativeFullscreen',
);
}
} catch (exception, stacktrace) {
debugPrint(exception.toString());
debugPrint(stacktrace.toString());
}
}
//竖屏
Future<void> verticalScreen() async {
@@ -27,14 +47,24 @@ Future<void> enterFullScreen() async {
//退出全屏显示
Future<void> exitFullScreen() async {
late SystemUiMode mode;
if ((Platform.isAndroid &&
(await DeviceInfoPlugin().androidInfo).version.sdkInt >= 29) ||
!Platform.isAndroid) {
mode = SystemUiMode.edgeToEdge;
} else {
mode = SystemUiMode.manual;
dynamic document;
try {
if (kIsWeb) {
document.exitFullscreen();
} else if (Platform.isAndroid || Platform.isIOS) {
await SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values,
);
await SystemChrome.setPreferredOrientations([]);
} else if (Platform.isMacOS || Platform.isWindows || Platform.isLinux) {
await const MethodChannel('com.alexmercerind/media_kit_video')
.invokeMethod(
'Utils.ExitNativeFullscreen',
);
}
} catch (exception, stacktrace) {
debugPrint(exception.toString());
debugPrint(stacktrace.toString());
}
await SystemChrome.setEnabledSystemUIMode(mode,
overlays: [SystemUiOverlay.top, SystemUiOverlay.bottom]);
}

View File

@@ -3,20 +3,23 @@ import 'dart:async';
import 'package:audio_video_progress_bar/audio_video_progress_bar.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_volume_controller/flutter_volume_controller.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
import 'package:pilipala/common/widgets/app_bar_ani.dart';
import 'package:pilipala/plugin/pl_player/controller.dart';
import 'package:pilipala/plugin/pl_player/models/duration.dart';
import 'package:pilipala/plugin/pl_player/models/fullscreen_mode.dart';
import 'package:pilipala/plugin/pl_player/models/play_status.dart';
import 'package:pilipala/plugin/pl_player/utils.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:volume_controller/volume_controller.dart';
import 'utils/fullscreen.dart';
import 'widgets/app_bar_ani.dart';
import 'widgets/backward_seek.dart';
import 'widgets/bottom_control.dart';
import 'widgets/common_btn.dart';
@@ -62,6 +65,9 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
bool _volumeInterceptEventStream = false;
Box setting = GStrorage.setting;
late FullScreenMode mode;
void onDoubleTapSeekBackward() {
setState(() {
_mountSeekBackwardButton = true;
@@ -84,9 +90,9 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
Future.microtask(() async {
try {
VolumeController().showSystemUI = false;
_volumeValue = await VolumeController().getVolume();
VolumeController().listener((value) {
FlutterVolumeController.showSystemUI = true;
_volumeValue = (await FlutterVolumeController.getVolume())!;
FlutterVolumeController.addListener((value) {
if (mounted && !_volumeInterceptEventStream) {
setState(() {
_volumeValue = value;
@@ -112,7 +118,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
Future<void> setVolume(double value) async {
try {
VolumeController().setVolume(value);
FlutterVolumeController.showSystemUI = false;
await FlutterVolumeController.setVolume(value);
} catch (_) {}
setState(() {
_volumeValue = value;
@@ -149,16 +156,37 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
Future<void> triggerFullScreen() async {
PlPlayerController _ = widget.controller;
mode = FullScreenModeCode.fromCode(
setting.get(SettingBoxKey.fullScreenMode, defaultValue: 0))!;
if (!_.isFullScreen.value) {
/// 按照视频宽高比决定全屏方向
if (_.direction.value == 'horizontal') {
/// 进入全屏
await enterFullScreen();
// 横
// await landScape();
} else {
// 竖屏
await verticalScreen();
switch (mode) {
case FullScreenMode.auto:
if (_.direction.value == 'horizontal') {
/// 进入全
await enterFullScreen();
// 横屏
await landScape();
} else {
// 竖屏
await verticalScreen();
}
break;
case FullScreenMode.vertical:
/// 进入全屏
await enterFullScreen();
// 横屏
await verticalScreen();
break;
case FullScreenMode.horizontal:
/// 进入全屏
await enterFullScreen();
// 横屏
await landScape();
break;
}
_.toggleFullScreen(true);
@@ -235,7 +263,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
),
),
/// 长按倍速
/// 长按倍速 toast
Obx(
() => Align(
alignment: Alignment.topCenter,
@@ -274,7 +302,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
),
),
/// 时间进度
/// 时间进度 toast
Obx(
() => Align(
alignment: Alignment.topCenter,
@@ -426,6 +454,24 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
),
),
Obx(() {
if (_.buffered.value == Duration.zero) {
return Positioned.fill(
child: Container(
color: Colors.black,
child: Center(
child: Image.asset(
'assets/images/loading.gif',
height: 25,
),
),
),
);
} else {
return Container();
}
}),
/// 手势
Positioned.fill(
left: 16,
@@ -437,6 +483,10 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
_.controls = !_.showControls.value;
},
onDoubleTapDown: (details) {
// live模式下禁用
if (_.videoType.value == 'live') {
return;
}
final totalWidth = MediaQuery.of(context).size.width;
final tapPosition = details.localPosition.dx;
final sectionWidth = totalWidth / 3;
@@ -456,17 +506,17 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
},
onLongPressStart: (detail) {
feedBack();
double currentSpeed = _.playbackSpeed;
_.setDoubleSpeedStatus(true);
_.setPlaybackSpeed(currentSpeed * 2);
},
onLongPressEnd: (details) {
double currentSpeed = _.playbackSpeed;
_.setDoubleSpeedStatus(false);
_.setPlaybackSpeed(currentSpeed / 2);
},
// 水平位置 快进
/// 水平位置 快进 live模式下禁用
onHorizontalDragUpdate: (DragUpdateDetails details) {
if (_.videoType.value == 'live') {
return;
}
final tapPosition = details.localPosition.dx;
int curSliderPosition = _.sliderPosition.value.inSeconds;
late int result;
@@ -485,6 +535,9 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
_initTapPositoin = tapPosition;
},
onHorizontalDragEnd: (DragEndDetails details) {
if (_.videoType.value == 'live') {
return;
}
_.onChangedSliderEnd();
_.seekTo(_.sliderPosition.value);
},
@@ -517,8 +570,6 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
_distance = 0.0;
}
_distance = dy;
// triggerFullScreen();
} else {
// 右边区域 👈
final volume = _volumeValue - delta / 100.0;
@@ -531,9 +582,10 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
),
// 头部、底部控制条
if (_.controlsEnabled)
Obx(
() => Column(
Obx(
() => Visibility(
visible: _.videoType.value != 'live',
child: Column(
children: [
if (widget.headerControl != null)
ClipRect(
@@ -560,7 +612,9 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
],
),
),
// 进度条
),
/// 进度条 live模式下禁用
Obx(
() {
final int value = _.sliderPosition.value.inSeconds;
@@ -613,9 +667,10 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
),
// 锁
if (_.controlsEnabled)
Obx(
() => Align(
Obx(
() => Visibility(
visible: _.videoType.value != 'live',
child: Align(
alignment: Alignment.centerLeft,
child: FractionalTranslation(
translation: const Offset(0.5, 0.0),
@@ -635,6 +690,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
),
),
),
),
//
Obx(() {
if (_.dataStatus.loading || _.isBuffering.value) {
@@ -676,7 +732,6 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
},
child: BackwardSeekIndicator(
onChanged: (value) {
print(value);
// _seekBarDeltaValueNotifier.value = -value;
},
onSubmitted: (value) {

Some files were not shown because too many files have changed in this diff Show More