mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
merge main
This commit is contained in:
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
],
|
||||
)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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©right=-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
36
lib/http/bangumi.dart
Normal 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
26
lib/http/black.dart
Normal 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'],
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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']};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 [
|
||||
|
||||
90
lib/models/bangumi/list.dart
Normal file
90
lib/models/bangumi/list.dart
Normal 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'];
|
||||
}
|
||||
}
|
||||
55
lib/models/common/tab_type.dart
Normal file
55
lib/models/common/tab_type.dart
Normal 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(),
|
||||
},
|
||||
];
|
||||
13
lib/models/common/theme_type.dart
Normal file
13
lib/models/common/theme_type.dart
Normal 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];
|
||||
}
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
45
lib/models/github/latest.dart
Normal file
45
lib/models/github/latest.dart
Normal 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'];
|
||||
}
|
||||
}
|
||||
@@ -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"] ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
209
lib/models/home/rcmd/result.g.dart
Normal file
209
lib/models/home/rcmd/result.g.dart
Normal 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;
|
||||
}
|
||||
@@ -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'];
|
||||
|
||||
37
lib/models/user/black.dart
Normal file
37
lib/models/user/black.dart
Normal 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'];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'];
|
||||
|
||||
@@ -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
246
lib/pages/about/index.dart
Normal 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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: '转发'),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
// )
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ]
|
||||
// ],
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
184
lib/pages/bangumi/widgets/bangumu_card_v.dart
Normal file
184
lib/pages/bangumi/widgets/bangumu_card_v.dart
Normal 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
156
lib/pages/blacklist/index.dart
Normal file
156
lib/pages/blacklist/index.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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'];
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -40,7 +40,7 @@ class _UpPanelState extends State<UpPanel> {
|
||||
1,
|
||||
UpItem(
|
||||
face: user.get(UserBoxKey.userFace),
|
||||
uname: '我的',
|
||||
uname: '我',
|
||||
mid: user.get(UserBoxKey.userMid),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
// 清空观看历史
|
||||
|
||||
@@ -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))
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -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) ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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('确认'),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
// ),
|
||||
// ),
|
||||
// ),
|
||||
// ]),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -88,6 +88,7 @@ class _SearchResultPageState extends State<SearchResultPage>
|
||||
tag: SearchType.values[index].type)
|
||||
.animateToTop();
|
||||
}
|
||||
|
||||
_searchResultController!.tabIndex = index;
|
||||
},
|
||||
),
|
||||
|
||||
@@ -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('确认'),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// 开启关闭震动反馈
|
||||
|
||||
170
lib/pages/setting/play_setting.dart
Normal file
170
lib/pages/setting/play_setting.dart
Normal 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),
|
||||
),
|
||||
]
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
58
lib/pages/setting/privacy_setting.dart
Normal file
58
lib/pages/setting/privacy_setting.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
191
lib/pages/setting/style_setting.dart
Normal file
191
lib/pages/setting/style_setting.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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('关于'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
120
lib/pages/setting/widgets/select_item.dart
Normal file
120
lib/pages/setting/widgets/select_item.dart
Normal 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],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
69
lib/pages/setting/widgets/switch_item.dart
Normal file
69
lib/pages/setting/widgets/switch_item.dart
Normal 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(() {});
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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('完成'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
203
lib/pages/video/detail/introduction/widgets/page.dart
Normal file
203
lib/pages/video/detail/introduction/widgets/page.dart
Normal 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,
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -38,6 +38,7 @@ class _SeasonPanelState extends State<SeasonPanel> {
|
||||
item.cid,
|
||||
item.aid,
|
||||
);
|
||||
currentIndex = i;
|
||||
Get.back();
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user