Merge branch 'design' into alpha

This commit is contained in:
guozhigq
2023-09-29 23:13:54 +08:00
29 changed files with 1113 additions and 227 deletions

View File

@ -306,4 +306,14 @@ class Api {
static const String onlineTotal = '/x/player/online/total'; static const String onlineTotal = '/x/player/online/total';
static const String webDanmaku = '/x/v2/dm/web/seg.so'; static const String webDanmaku = '/x/v2/dm/web/seg.so';
// up主分组
static const String followUpTag = '/x/relation/tags';
// 设置Up主分组
// 0 添加至默认分组 否则使用,分割tagid
static const String addUsers = '/x/relation/tags/addUsers';
// 获取指定分组下的up
static const String followUpGroup = '/x/relation/tag';
} }

View File

@ -1,7 +1,9 @@
import 'package:pilipala/http/index.dart'; import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/dynamics/result.dart'; import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/models/member/archive.dart'; import 'package:pilipala/models/member/archive.dart';
import 'package:pilipala/models/member/info.dart'; import 'package:pilipala/models/member/info.dart';
import 'package:pilipala/models/member/tags.dart';
import 'package:pilipala/utils/wbi_sign.dart'; import 'package:pilipala/utils/wbi_sign.dart';
class MemberHttp { class MemberHttp {
@ -144,4 +146,73 @@ class MemberHttp {
}; };
} }
} }
// 查询分组
static Future followUpTags() async {
var res = await Request().get(Api.followUpTag);
if (res.data['code'] == 0) {
return {
'status': true,
'data': res.data['data']
.map<MemberTagItemModel>((e) => MemberTagItemModel.fromJson(e))
.toList()
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 设置分组
static Future addUsers(int? fids, String? tagids) async {
var res = await Request().post(Api.addUsers, queryParameters: {
'fids': fids,
'tagids': tagids ?? '0',
'csrf': await Request.getCsrf(),
}, data: {
'cross_domain': true
});
if (res.data['code'] == 0) {
return {'status': true, 'data': [], 'msg': '操作成功'};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
// 获取某分组下的up
static Future followUpGroup(
int? mid,
int? tagid,
int? pn,
int? ps,
) async {
var res = await Request().get(Api.followUpGroup, data: {
'mid': mid,
'tagid': tagid,
'pn': pn,
'ps': ps,
});
if (res.data['code'] == 0) {
// FollowItemModel
return {
'status': true,
'data': res.data['data']
.map<FollowItemModel>((e) => FollowItemModel.fromJson(e))
.toList()
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
} }

View File

@ -1,13 +1,16 @@
import 'dart:convert'; import 'dart:convert';
import 'package:hive/hive.dart';
import 'package:pilipala/http/index.dart'; import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/bangumi/info.dart'; import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/search_type.dart'; import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/search/hot.dart'; import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/search/result.dart'; import 'package:pilipala/models/search/result.dart';
import 'package:pilipala/models/search/suggest.dart'; import 'package:pilipala/models/search/suggest.dart';
import 'package:pilipala/utils/storage.dart';
class SearchHttp { class SearchHttp {
static Box setting = GStrorage.setting;
static Future hotSearchList() async { static Future hotSearchList() async {
var res = await Request().get(Api.hotSearchList); var res = await Request().get(Api.hotSearchList);
if (res.data is String) { if (res.data is String) {
@ -78,6 +81,12 @@ class SearchHttp {
try { try {
switch (searchType) { switch (searchType) {
case SearchType.video: case SearchType.video:
List<int> blackMidsList =
setting.get(SettingBoxKey.blackMidsList, defaultValue: [-1]);
for (var i in res.data['data']['result']) {
// 屏蔽推广和拉黑用户
i['available'] = !blackMidsList.contains(i['mid']);
}
data = SearchVideoModel.fromJson(res.data['data']); data = SearchVideoModel.fromJson(res.data['data']);
break; break;
case SearchType.live_room: case SearchType.live_room:

View File

@ -8,7 +8,7 @@ class FollowDataModel {
List<FollowItemModel>? list; List<FollowItemModel>? list;
FollowDataModel.fromJson(Map<String, dynamic> json) { FollowDataModel.fromJson(Map<String, dynamic> json) {
total = json['total']; total = json['total'] ?? 0;
list = json['list'] list = json['list']
.map<FollowItemModel>((e) => FollowItemModel.fromJson(e)) .map<FollowItemModel>((e) => FollowItemModel.fromJson(e))
.toList(); .toList();
@ -19,7 +19,7 @@ class FollowItemModel {
FollowItemModel({ FollowItemModel({
this.mid, this.mid,
this.attribute, this.attribute,
this.mtime, // this.mtime,
this.tag, this.tag,
this.special, this.special,
this.uname, this.uname,
@ -30,7 +30,7 @@ class FollowItemModel {
int? mid; int? mid;
int? attribute; int? attribute;
int? mtime; // int? mtime;
List? tag; List? tag;
int? special; int? special;
String? uname; String? uname;
@ -41,7 +41,7 @@ class FollowItemModel {
FollowItemModel.fromJson(Map<String, dynamic> json) { FollowItemModel.fromJson(Map<String, dynamic> json) {
mid = json['mid']; mid = json['mid'];
attribute = json['attribute']; attribute = json['attribute'];
mtime = json['mtime']; // mtime = json['mtime'];
tag = json['tag']; tag = json['tag'];
special = json['special']; special = json['special'];
uname = json['uname']; uname = json['uname'];

View File

@ -0,0 +1,23 @@
class MemberTagItemModel {
MemberTagItemModel({
this.count,
this.name,
this.tagid,
this.tip,
this.checked,
});
int? count;
String? name;
int? tagid;
String? tip;
bool? checked;
MemberTagItemModel.fromJson(Map<String, dynamic> json) {
count = json['count'];
name = json['name'];
tagid = json['tagid'];
tip = json['tip'];
checked = false;
}
}

View File

@ -6,6 +6,7 @@ class SearchVideoModel {
List<SearchVideoItemModel>? list; List<SearchVideoItemModel>? list;
SearchVideoModel.fromJson(Map<String, dynamic> json) { SearchVideoModel.fromJson(Map<String, dynamic> json) {
list = json['result'] list = json['result']
.where((e) => e['available'] == true)
.map<SearchVideoItemModel>((e) => SearchVideoItemModel.fromJson(e)) .map<SearchVideoItemModel>((e) => SearchVideoItemModel.fromJson(e))
.toList(); .toList();
} }
@ -17,7 +18,7 @@ class SearchVideoItemModel {
this.id, this.id,
this.cid, this.cid,
// this.author, // this.author,
// this.mid, this.mid,
// this.typeid, // this.typeid,
// this.typename, // this.typename,
this.arcurl, this.arcurl,
@ -47,7 +48,7 @@ class SearchVideoItemModel {
int? id; int? id;
int? cid; int? cid;
// String? author; // String? author;
// String? mid; int? mid;
// String? typeid; // String? typeid;
// String? typename; // String? typename;
String? arcurl; String? arcurl;
@ -80,6 +81,7 @@ class SearchVideoItemModel {
arcurl = json['arcurl']; arcurl = json['arcurl'];
aid = json['aid']; aid = json['aid'];
bvid = json['bvid']; bvid = json['bvid'];
mid = json['mid'];
// title = json['title'].replaceAll(RegExp(r'<.*?>'), ''); // title = json['title'].replaceAll(RegExp(r'<.*?>'), '');
title = Em.regTitle(json['title']); title = Em.regTitle(json['title']);
description = json['description']; description = json['description'];

View File

@ -9,6 +9,7 @@ import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/user/fav_folder.dart'; import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart'; import 'package:pilipala/pages/video/detail/reply/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
@ -21,7 +22,7 @@ class BangumiIntroController extends GetxController {
? int.parse(Get.parameters['seasonId']!) ? int.parse(Get.parameters['seasonId']!)
: null; : null;
var epId = Get.parameters['epId'] != null var epId = Get.parameters['epId'] != null
? int.parse(Get.parameters['epId']!) ? int.tryParse(Get.parameters['epId']!)
: null; : null;
// 是否预渲染 骨架屏 // 是否预渲染 骨架屏
@ -257,7 +258,7 @@ class BangumiIntroController extends GetxController {
VideoDetailController videoDetailCtr = VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']); Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
videoDetailCtr.bvid = bvid; videoDetailCtr.bvid = bvid;
videoDetailCtr.cid = cid; videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid; videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.queryVideoUrl(); videoDetailCtr.queryVideoUrl();
// 重新请求评论 // 重新请求评论
@ -292,4 +293,31 @@ class BangumiIntroController extends GetxController {
} }
return result; return result;
} }
/// 列表循环或者顺序播放时,自动播放下一个
void nextPlay() {
late List episodes;
if (bangumiDetail.value.episodes != null) {
episodes = bangumiDetail.value.episodes!;
}
VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']);
int currentIndex =
episodes.indexWhere((e) => e.cid == videoDetailCtr.cid.value);
int nextIndex = currentIndex + 1;
PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
// 列表循环
if (platRepeat == PlayRepeat.listCycle) {
if (nextIndex == episodes.length - 1) {
nextIndex = 0;
}
}
if (nextIndex <= episodes.length - 1 &&
platRepeat == PlayRepeat.listOrder) {}
int cid = episodes[nextIndex].cid!;
String bvid = episodes[nextIndex].bvid!;
int aid = episodes[nextIndex].aid!;
changeSeasonOrbangu(bvid, cid, aid);
}
} }

View File

@ -34,10 +34,12 @@ class BangumiIntroPanel extends StatefulWidget {
class _BangumiIntroPanelState extends State<BangumiIntroPanel> class _BangumiIntroPanelState extends State<BangumiIntroPanel>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin {
final BangumiIntroController bangumiIntroController = late BangumiIntroController bangumiIntroController;
Get.put(BangumiIntroController(), tag: Get.arguments['heroTag']); late VideoDetailController videoDetailCtr;
BangumiInfoModel? bangumiDetail; BangumiInfoModel? bangumiDetail;
late Future _futureBuilderFuture; late Future _futureBuilderFuture;
late int cid;
late String heroTag;
// 添加页面缓存 // 添加页面缓存
@override @override
@ -46,10 +48,19 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
heroTag = Get.arguments['heroTag'];
cid = widget.cid!;
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
bangumiIntroController.bangumiDetail.listen((value) { bangumiIntroController.bangumiDetail.listen((value) {
bangumiDetail = value; bangumiDetail = value;
}); });
_futureBuilderFuture = bangumiIntroController.queryBangumiIntro(); _futureBuilderFuture = bangumiIntroController.queryBangumiIntro();
videoDetailCtr.cid.listen((p0) {
print('🐶🐶$p0');
cid = p0;
setState(() {});
});
} }
@override @override
@ -61,9 +72,11 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.data['status']) { if (snapshot.data['status']) {
// 请求成功 // 请求成功
return BangumiInfo( return BangumiInfo(
loadingStatus: false, loadingStatus: false,
bangumiDetail: bangumiDetail, bangumiDetail: bangumiDetail,
cid: cid,
); );
} else { } else {
// 请求错误 // 请求错误
@ -77,7 +90,7 @@ class _BangumiIntroPanelState extends State<BangumiIntroPanel>
return BangumiInfo( return BangumiInfo(
loadingStatus: true, loadingStatus: true,
bangumiDetail: bangumiDetail, bangumiDetail: bangumiDetail,
cid: widget.cid, cid: cid,
); );
} }
}, },
@ -118,6 +131,12 @@ class _BangumiInfoState extends State<BangumiInfo> {
bangumiItem = bangumiIntroController.bangumiItem; bangumiItem = bangumiIntroController.bangumiItem;
sheetHeight = localCache.get('sheetHeight'); sheetHeight = localCache.get('sheetHeight');
cid = widget.cid!; cid = widget.cid!;
print('cid: $cid');
videoDetailCtr.cid.listen((p0) {
cid = p0;
print('cid: $cid');
setState(() {});
});
} }
// 收藏 // 收藏

View File

@ -1,7 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/models/bangumi/info.dart'; import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class BangumiPanel extends StatefulWidget { class BangumiPanel extends StatefulWidget {
@ -30,16 +32,28 @@ class _BangumiPanelState extends State<BangumiPanel> {
dynamic userInfo; dynamic userInfo;
// 默认未开通 // 默认未开通
int vipStatus = 0; int vipStatus = 0;
late int cid;
String heroTag = Get.arguments['heroTag'];
late final VideoDetailController videoDetailCtr;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
currentIndex = widget.pages.indexWhere((e) => e.cid == widget.cid!); cid = widget.cid!;
currentIndex = widget.pages.indexWhere((e) => e.cid == cid);
scrollToIndex(); scrollToIndex();
userInfo = userInfoCache.get('userInfoCache'); userInfo = userInfoCache.get('userInfoCache');
if (userInfo != null) { if (userInfo != null) {
vipStatus = userInfo.vipStatus; vipStatus = userInfo.vipStatus;
} }
videoDetailCtr = Get.find<VideoDetailController>(tag: heroTag);
videoDetailCtr.cid.listen((p0) {
cid = p0;
setState(() {});
currentIndex = widget.pages.indexWhere((e) => e.cid == cid);
scrollToIndex();
});
} }
@override @override

View File

@ -177,7 +177,7 @@ class _DynamicDetailPageState extends State<DynamicDetailPage>
return AnimatedOpacity( return AnimatedOpacity(
opacity: snapshot.data ? 1 : 0, opacity: snapshot.data ? 1 : 0,
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
child: author(_dynamicDetailController!.item, context), child: AuthorPanel(item: _dynamicDetailController.item),
); );
}, },
), ),

View File

@ -1,65 +1,159 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/http/user.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
Widget author(item, context) { class AuthorPanel extends StatelessWidget {
String heroTag = Utils.makeHeroTag(item.modules.moduleAuthor.mid); final dynamic item;
return Row( const AuthorPanel({super.key, required this.item});
children: [
GestureDetector( @override
onTap: () { Widget build(BuildContext context) {
feedBack(); String heroTag = Utils.makeHeroTag(item.modules.moduleAuthor.mid);
Get.toNamed( return Row(
'/member?mid=${item.modules.moduleAuthor.mid}', children: [
arguments: { GestureDetector(
'face': item.modules.moduleAuthor.face, onTap: () {
'heroTag': heroTag feedBack();
}, Get.toNamed(
); '/member?mid=${item.modules.moduleAuthor.mid}',
}, arguments: {
child: Hero( 'face': item.modules.moduleAuthor.face,
tag: heroTag, 'heroTag': heroTag
child: NetworkImgLayer( },
width: 40, );
height: 40, },
type: 'avatar', child: Hero(
src: item.modules.moduleAuthor.face, tag: heroTag,
child: NetworkImgLayer(
width: 40,
height: 40,
type: 'avatar',
src: item.modules.moduleAuthor.face,
),
), ),
), ),
), const SizedBox(width: 10),
const SizedBox(width: 10), Column(
Column( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Text(
Text( item.modules.moduleAuthor.name,
item.modules.moduleAuthor.name, style: TextStyle(
style: TextStyle( color: item.modules.moduleAuthor!.vip != null &&
color: item.modules.moduleAuthor!.vip != null && item.modules.moduleAuthor!.vip['status'] > 0
item.modules.moduleAuthor!.vip['status'] > 0 ? const Color.fromARGB(255, 251, 100, 163)
? const Color.fromARGB(255, 251, 100, 163) : Theme.of(context).colorScheme.onBackground,
: Theme.of(context).colorScheme.onBackground, fontSize: Theme.of(context).textTheme.titleSmall!.fontSize,
fontSize: Theme.of(context).textTheme.titleSmall!.fontSize, ),
),
DefaultTextStyle.merge(
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize,
),
child: Row(
children: [
Text(item.modules.moduleAuthor.pubTime),
if (item.modules.moduleAuthor.pubTime != '' &&
item.modules.moduleAuthor.pubAction != '')
const Text(' '),
Text(item.modules.moduleAuthor.pubAction),
],
),
)
],
),
const Spacer(),
if (item.type == 'DYNAMIC_TYPE_AV')
SizedBox(
width: 32,
height: 32,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
onPressed: () {
showModalBottomSheet(
context: context,
useRootNavigator: true,
isScrollControlled: true,
builder: (context) {
return MorePanel(item: item);
},
);
},
icon: const Icon(Icons.more_vert_outlined, size: 18),
), ),
), ),
DefaultTextStyle.merge( ],
style: TextStyle( );
color: Theme.of(context).colorScheme.outline, }
fontSize: Theme.of(context).textTheme.labelSmall!.fontSize, }
class MorePanel extends StatelessWidget {
final dynamic item;
const MorePanel({super.key, required this.item});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom),
// clipBehavior: Clip.hardEdge,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
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.outline,
borderRadius: const BorderRadius.all(Radius.circular(3))),
),
),
), ),
child: Row( ),
children: [ ListTile(
Text(item.modules.moduleAuthor.pubTime), onTap: () async {
if (item.modules.moduleAuthor.pubTime != '' && try {
item.modules.moduleAuthor.pubAction != '') String bvid = item.modules.moduleDynamic.major.archive.bvid;
const Text(' '), var res = await UserHttp.toViewLater(bvid: bvid);
Text(item.modules.moduleAuthor.pubAction), SmartDialog.showToast(res['msg']);
], Get.back();
} catch (err) {
SmartDialog.showToast('出错了:${err.toString()}');
}
},
minLeadingWidth: 0,
// dense: true,
leading: const Icon(Icons.watch_later_outlined, size: 19),
title: Text(
'稍后再看',
style: Theme.of(context).textTheme.titleSmall,
), ),
) ),
const Divider(thickness: 0.1, height: 1),
ListTile(
onTap: () => Get.back(),
minLeadingWidth: 0,
dense: true,
title: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
textAlign: TextAlign.center,
),
),
], ],
), ),
], );
); }
} }

View File

@ -39,7 +39,7 @@ class DynamicPanel extends StatelessWidget {
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.fromLTRB(12, 12, 12, 8), padding: const EdgeInsets.fromLTRB(12, 12, 12, 8),
child: author(item, context), child: AuthorPanel(item: item),
), ),
if (item!.modules!.moduleDynamic!.desc != null || if (item!.modules!.moduleDynamic!.desc != null ||
item!.modules!.moduleDynamic!.major != null) item!.modules!.moduleDynamic!.major != null)

View File

@ -1,20 +1,28 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:pilipala/http/follow.dart'; import 'package:pilipala/http/follow.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/follow/result.dart'; import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/models/member/tags.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class FollowController extends GetxController { /// 查看自己的关注时,可以查看分类
/// 查看其他人的关注时,只可以看全部
class FollowController extends GetxController with GetTickerProviderStateMixin {
Box userInfoCache = GStrorage.userInfo; Box userInfoCache = GStrorage.userInfo;
int pn = 1; int pn = 1;
int ps = 20; int ps = 20;
int total = 0; int total = 0;
RxList<FollowItemModel> followList = [FollowItemModel()].obs; RxList<FollowItemModel> followList = <FollowItemModel>[].obs;
late int mid; late int mid;
late String name; late String name;
var userInfo; var userInfo;
RxString loadingText = '加载中...'.obs; RxString loadingText = '加载中...'.obs;
RxBool isOwner = false.obs;
late List<MemberTagItemModel> followTags;
late TabController tabController;
@override @override
void onInit() { void onInit() {
@ -23,6 +31,7 @@ class FollowController extends GetxController {
mid = Get.parameters['mid'] != null mid = Get.parameters['mid'] != null
? int.parse(Get.parameters['mid']!) ? int.parse(Get.parameters['mid']!)
: userInfo.mid; : userInfo.mid;
isOwner.value = mid == userInfo.mid;
name = Get.parameters['name'] ?? userInfo.uname; name = Get.parameters['name'] ?? userInfo.uname;
} }
@ -56,4 +65,20 @@ class FollowController extends GetxController {
} }
return res; return res;
} }
// 当查看当前用户的关注时,请求关注分组
Future followUpTags() async {
if (userInfo != null && mid == userInfo.mid) {
var res = await MemberHttp.followUpTags();
if (res['status']) {
followTags = res['data'];
tabController = TabController(
initialIndex: 0,
length: res['data'].length,
vsync: this,
);
}
return res;
}
}
} }

View File

@ -1,12 +1,8 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/models/follow/result.dart';
import 'controller.dart'; import 'controller.dart';
import 'widgets/follow_item.dart'; import 'widgets/follow_list.dart';
import 'widgets/owner_follow_list.dart';
class FollowPage extends StatefulWidget { class FollowPage extends StatefulWidget {
const FollowPage({super.key}); const FollowPage({super.key});
@ -19,30 +15,12 @@ class _FollowPageState extends State<FollowPage> {
late String mid; late String mid;
late FollowController _followController; late FollowController _followController;
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
Future? _futureBuilderFuture;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
mid = Get.parameters['mid']!; mid = Get.parameters['mid']!;
_followController = Get.put(FollowController(), tag: mid); _followController = Get.put(FollowController(), tag: mid);
_futureBuilderFuture = _followController.queryFollowings('init');
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
_followController.queryFollowings('onLoad');
});
}
},
);
}
@override
void dispose() {
scrollController.removeListener(() {});
super.dispose();
} }
@override @override
@ -54,73 +32,57 @@ class _FollowPageState extends State<FollowPage> {
titleSpacing: 0, titleSpacing: 0,
centerTitle: false, centerTitle: false,
title: Text( title: Text(
'${_followController.name}的关注', _followController.isOwner.value
? '我的关注'
: '${_followController.name}的关注',
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
), ),
body: RefreshIndicator( body: Obx(
onRefresh: () async => () => !_followController.isOwner.value
await _followController.queryFollowings('init'), ? FollowList(ctr: _followController)
child: FutureBuilder( : FutureBuilder(
future: _futureBuilderFuture, future: _followController.followUpTags(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
var data = snapshot.data; var data = snapshot.data;
if (data['status']) { if (data['status']) {
List<FollowItemModel> list = _followController.followList; return Column(
return Obx( children: [
() => list.isNotEmpty TabBar(
? ListView.builder( controller: _followController.tabController,
controller: scrollController, isScrollable: true,
itemCount: list.length + 1, tabs: [
itemBuilder: (BuildContext context, int index) { for (var i in data['data']) ...[
if (index == list.length) { Tab(text: i.name),
return Container( ]
height: ]),
MediaQuery.of(context).padding.bottom + Expanded(
60, child: TabBarView(
padding: EdgeInsets.only( controller: _followController.tabController,
bottom: MediaQuery.of(context) children: [
.padding for (var i = 0;
.bottom), i < _followController.tabController.length;
child: Center( i++) ...[
child: Obx( OwnerFollowList(
() => Text( ctr: _followController,
_followController.loadingText.value, tagItem: _followController.followTags[i],
style: TextStyle( )
color: Theme.of(context) ]
.colorScheme ],
.outline, ),
fontSize: 13),
),
),
),
);
} else {
return followItem(item: list[index]);
}
},
)
: const CustomScrollView(
slivers: [NoData()],
), ),
); ],
} else { );
return CustomScrollView( } else {
slivers: [ return const SizedBox();
HttpError( }
errMsg: data['msg'], } else {
fn: () => _followController.queryFollowings('init'), return const SizedBox();
) }
], },
); ),
} ),
} else {
// 骨架屏
return const SizedBox();
}
},
)),
); );
} }
} }

View File

@ -1,38 +1,45 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/common/widgets/network_img_layer.dart'; import 'package:pilipala/common/widgets/network_img_layer.dart';
import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/utils.dart'; import 'package:pilipala/utils/utils.dart';
Widget followItem({item}) { class FollowItem extends StatelessWidget {
String heroTag = Utils.makeHeroTag(item!.mid); final FollowItemModel item;
return ListTile( const FollowItem({super.key, required this.item});
onTap: () {
feedBack(); @override
Get.toNamed('/member?mid=${item.mid}', Widget build(BuildContext context) {
arguments: {'face': item.face, 'heroTag': heroTag}); String heroTag = Utils.makeHeroTag(item!.mid);
}, return ListTile(
leading: Hero( onTap: () {
tag: heroTag, feedBack();
child: NetworkImgLayer( Get.toNamed('/member?mid=${item.mid}',
width: 45, arguments: {'face': item.face, 'heroTag': heroTag});
height: 45, },
type: 'avatar', leading: Hero(
src: item.face, tag: heroTag,
child: NetworkImgLayer(
width: 45,
height: 45,
type: 'avatar',
src: item.face,
),
), ),
), title: Text(
title: Text( item.uname!,
item.uname, maxLines: 1,
maxLines: 1, overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 14),
style: const TextStyle(fontSize: 14), ),
), subtitle: Text(
subtitle: Text( item.sign!,
item.sign, maxLines: 1,
maxLines: 1, overflow: TextOverflow.ellipsis,
overflow: TextOverflow.ellipsis, ),
), dense: true,
dense: true, trailing: const SizedBox(width: 6),
trailing: const SizedBox(width: 6), );
); }
} }

View File

@ -0,0 +1,111 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/pages/follow/index.dart';
import 'follow_item.dart';
class FollowList extends StatefulWidget {
final FollowController ctr;
const FollowList({
super.key,
required this.ctr,
});
@override
State<FollowList> createState() => _FollowListState();
}
class _FollowListState extends State<FollowList> {
late Future _futureBuilderFuture;
final ScrollController scrollController = ScrollController();
@override
void initState() {
super.initState();
_futureBuilderFuture = widget.ctr.queryFollowings('init');
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
widget.ctr.queryFollowings('onLoad');
});
}
},
);
}
@override
void dispose() {
scrollController.removeListener(() {});
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: () async => await widget.ctr.queryFollowings('init'),
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
var data = snapshot.data;
if (data['status']) {
List<FollowItemModel> list = widget.ctr.followList;
return Obx(
() => list.isNotEmpty
? ListView.builder(
controller: scrollController,
itemCount: list.length + 1,
itemBuilder: (BuildContext context, int index) {
if (index == list.length) {
return Container(
height:
MediaQuery.of(context).padding.bottom + 60,
padding: EdgeInsets.only(
bottom:
MediaQuery.of(context).padding.bottom),
child: Center(
child: Obx(
() => Text(
widget.ctr.loadingText.value,
style: TextStyle(
color: Theme.of(context)
.colorScheme
.outline,
fontSize: 13),
),
),
),
);
} else {
return FollowItem(item: list[index]);
}
},
)
: const CustomScrollView(slivers: [NoData()]),
);
} else {
return CustomScrollView(
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => widget.ctr.queryFollowings('init'),
)
],
);
}
} else {
// 骨架屏
return const SizedBox();
}
},
),
);
}
}

View File

@ -0,0 +1,128 @@
import 'package:easy_debounce/easy_throttle.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/common/widgets/no_data.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/follow/result.dart';
import 'package:pilipala/models/member/tags.dart';
import 'package:pilipala/pages/follow/index.dart';
import 'follow_item.dart';
class OwnerFollowList extends StatefulWidget {
final FollowController ctr;
final MemberTagItemModel? tagItem;
const OwnerFollowList({super.key, required this.ctr, this.tagItem});
@override
State<OwnerFollowList> createState() => _OwnerFollowListState();
}
class _OwnerFollowListState extends State<OwnerFollowList>
with AutomaticKeepAliveClientMixin {
late int mid;
late Future _futureBuilderFuture;
final ScrollController scrollController = ScrollController();
int pn = 1;
int ps = 20;
late MemberTagItemModel tagItem;
RxList<FollowItemModel> followList = <FollowItemModel>[].obs;
@override
bool get wantKeepAlive => true;
@override
void initState() {
super.initState();
mid = widget.ctr.mid;
tagItem = widget.tagItem!;
_futureBuilderFuture = followUpGroup('init');
scrollController.addListener(
() async {
if (scrollController.position.pixels >=
scrollController.position.maxScrollExtent - 200) {
EasyThrottle.throttle('follow', const Duration(seconds: 1), () {
followUpGroup('onLoad');
});
}
},
);
}
// 获取分组下up
Future followUpGroup(type) async {
if (type == 'init') {
pn = 1;
}
var res = await MemberHttp.followUpGroup(mid, tagItem.tagid, pn, ps);
if (res['status']) {
if (res['data'].isNotEmpty) {
if (type == 'init') {
followList.value = res['data'];
} else {
followList.addAll(res['data']);
}
pn += 1;
}
}
return res;
}
@override
void dispose() {
scrollController.removeListener(() {});
scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
super.build(context);
return RefreshIndicator(
onRefresh: () async => await followUpGroup('init'),
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
var data = snapshot.data;
if (data['status']) {
return Obx(
() => followList.isNotEmpty
? ListView.builder(
controller: scrollController,
itemCount: followList.length + 1,
itemBuilder: (BuildContext context, int index) {
if (index == followList.length) {
return Container(
height:
MediaQuery.of(context).padding.bottom + 60,
padding: EdgeInsets.only(
bottom:
MediaQuery.of(context).padding.bottom),
);
} else {
return FollowItem(item: followList[index]);
}
},
)
: const CustomScrollView(slivers: [NoData()]),
);
} else {
return CustomScrollView(
slivers: [
HttpError(
errMsg: data['msg'],
fn: () => widget.ctr.queryFollowings('init'),
)
],
);
}
} else {
// 骨架屏
return const SizedBox();
}
},
),
);
}
}

View File

@ -24,7 +24,7 @@ class VideoDetailController extends GetxController
with GetSingleTickerProviderStateMixin { with GetSingleTickerProviderStateMixin {
/// 路由传参 /// 路由传参
String bvid = Get.parameters['bvid']!; String bvid = Get.parameters['bvid']!;
int cid = int.parse(Get.parameters['cid']!); RxInt cid = int.parse(Get.parameters['cid']!).obs;
RxInt danmakuCid = 0.obs; RxInt danmakuCid = 0.obs;
String heroTag = Get.arguments['heroTag']; String heroTag = Get.arguments['heroTag'];
// 视频详情 // 视频详情
@ -109,7 +109,7 @@ class VideoDetailController extends GetxController
localCache.get(LocalCacheKey.historyPause) == true) { localCache.get(LocalCacheKey.historyPause) == true) {
enableHeart = false; enableHeart = false;
} }
danmakuCid.value = cid; danmakuCid.value = cid.value;
/// ///
if (Platform.isAndroid) { if (Platform.isAndroid) {
@ -218,7 +218,7 @@ class VideoDetailController extends GetxController
// 默认1倍速 // 默认1倍速
speed: 1.0, speed: 1.0,
bvid: bvid, bvid: bvid,
cid: cid, cid: cid.value,
enableHeart: enableHeart, enableHeart: enableHeart,
isFirstTime: isFirstTime, isFirstTime: isFirstTime,
autoplay: autoplay, autoplay: autoplay,
@ -230,7 +230,7 @@ class VideoDetailController extends GetxController
// 视频链接 // 视频链接
Future queryVideoUrl() async { Future queryVideoUrl() async {
var result = await VideoHttp.videoUrl(cid: cid, bvid: bvid); var result = await VideoHttp.videoUrl(cid: cid.value, bvid: bvid);
if (result['status']) { if (result['status']) {
data = result['data']; data = result['data'];

View File

@ -11,11 +11,14 @@ import 'package:pilipala/models/user/fav_folder.dart';
import 'package:pilipala/models/video_detail_res.dart'; import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/controller.dart'; import 'package:pilipala/pages/video/detail/controller.dart';
import 'package:pilipala/pages/video/detail/reply/index.dart'; import 'package:pilipala/pages/video/detail/reply/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'widgets/group_panel.dart';
class VideoIntroController extends GetxController { class VideoIntroController extends GetxController {
// 视频bvid // 视频bvid
String bvid = Get.parameters['bvid']!; String bvid = Get.parameters['bvid']!;
@ -58,6 +61,7 @@ class VideoIntroController extends GetxController {
RxString total = '1'.obs; RxString total = '1'.obs;
Timer? timer; Timer? timer;
bool isPaused = false; bool isPaused = false;
String heroTag = Get.arguments['heroTag'];
@override @override
void onInit() { void onInit() {
@ -102,9 +106,10 @@ class VideoIntroController extends GetxController {
if (videoDetail.value.pages!.isNotEmpty && lastPlayCid.value == 0) { if (videoDetail.value.pages!.isNotEmpty && lastPlayCid.value == 0) {
lastPlayCid.value = videoDetail.value.pages!.first.cid!; lastPlayCid.value = videoDetail.value.pages!.first.cid!;
} }
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']) // Get.find<VideoDetailController>(tag: heroTag).tabs.value = [
.tabs // '简介',
.value = ['简介', '评论 ${result['data']!.stat!.reply}']; // '评论 ${result['data']!.stat!.reply}'
// ];
// 获取到粉丝数再返回 // 获取到粉丝数再返回
await queryUserStat(); await queryUserStat();
} }
@ -425,6 +430,20 @@ class VideoIntroController extends GetxController {
} }
followStatus['attribute'] = actionStatus; followStatus['attribute'] = actionStatus;
followStatus.refresh(); followStatus.refresh();
if (actionStatus == 2) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: const Text('关注成功'),
duration: const Duration(seconds: 2),
action: SnackBarAction(
label: '设置分组',
onPressed: setFollowGroup,
),
),
);
}
}
} }
SmartDialog.dismiss(); SmartDialog.dismiss();
}, },
@ -440,16 +459,16 @@ class VideoIntroController extends GetxController {
Future changeSeasonOrbangu(bvid, cid, aid) async { Future changeSeasonOrbangu(bvid, cid, aid) async {
// 重新获取视频资源 // 重新获取视频资源
VideoDetailController videoDetailCtr = VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: Get.arguments['heroTag']); Get.find<VideoDetailController>(tag: heroTag);
videoDetailCtr.bvid = bvid; videoDetailCtr.bvid = bvid;
videoDetailCtr.cid = cid; videoDetailCtr.cid.value = cid;
videoDetailCtr.danmakuCid.value = cid; videoDetailCtr.danmakuCid.value = cid;
videoDetailCtr.queryVideoUrl(); videoDetailCtr.queryVideoUrl();
// 重新请求评论 // 重新请求评论
try { try {
/// 未渲染回复组件时可能异常 /// 未渲染回复组件时可能异常
VideoReplyController videoReplyCtr = VideoReplyController videoReplyCtr =
Get.find<VideoReplyController>(tag: Get.arguments['heroTag']); Get.find<VideoReplyController>(tag: heroTag);
videoReplyCtr.aid = aid; videoReplyCtr.aid = aid;
videoReplyCtr.queryReplyList(type: 'init'); videoReplyCtr.queryReplyList(type: 'init');
} catch (_) {} } catch (_) {}
@ -486,4 +505,60 @@ class VideoIntroController extends GetxController {
} }
super.onClose(); super.onClose();
} }
/// 列表循环或者顺序播放时,自动播放下一个
void nextPlay() {
late List episodes;
// if (videoDetail.value.ugcSeason != null) {
// UgcSeason ugcSeason = videoDetail.value.ugcSeason!;
// List<SectionItem> sections = ugcSeason.sections!;
// for (int i = 0; i < sections.length; i++) {
// List<EpisodeItem> episodesList = sections[i].episodes!;
// for (int j = 0; j < episodesList.length; j++) {
// if (episodesList[j].cid == lastPlayCid.value) {
// episodes = episodesList;
// continue;
// }
// }
// }
// }
if (videoDetail.value.ugcSeason != null) {
UgcSeason ugcSeason = videoDetail.value.ugcSeason!;
List<SectionItem> sections = ugcSeason.sections!;
episodes = [];
for (int i = 0; i < sections.length; i++) {
List<EpisodeItem> episodesList = sections[i].episodes!;
episodes.addAll(episodesList);
}
}
int currentIndex = episodes.indexWhere((e) => e.cid == lastPlayCid.value);
int nextIndex = currentIndex + 1;
VideoDetailController videoDetailCtr =
Get.find<VideoDetailController>(tag: heroTag);
PlayRepeat platRepeat = videoDetailCtr.plPlayerController.playRepeat;
// 列表循环
if (nextIndex >= episodes.length) {
if (platRepeat == PlayRepeat.listCycle) {
nextIndex = 0;
}
if (platRepeat == PlayRepeat.listOrder) {
return;
}
}
int cid = episodes[nextIndex].cid!;
String bvid = episodes[nextIndex].bvid!;
int aid = episodes[nextIndex].aid!;
changeSeasonOrbangu(bvid, cid, aid);
}
// 设置关注分组
void setFollowGroup() {
Get.bottomSheet(
GroupPanel(mid: videoDetail.value.owner!.mid!),
isScrollControlled: true,
);
}
} }

View File

@ -330,17 +330,17 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
), ),
const SizedBox(height: 7), const SizedBox(height: 7),
// 点赞收藏转发 布局样式1 // 点赞收藏转发 布局样式1
SingleChildScrollView( // SingleChildScrollView(
padding: const EdgeInsets.only(top: 7, bottom: 7), // padding: const EdgeInsets.only(top: 7, bottom: 7),
scrollDirection: Axis.horizontal, // scrollDirection: Axis.horizontal,
child: actionRow( // child: actionRow(
context, // context,
videoIntroController, // videoIntroController,
videoDetailCtr, // videoDetailCtr,
), // ),
), // ),
// 点赞收藏转发 布局样式2 // 点赞收藏转发 布局样式2
// actionGrid(context, videoIntroController), actionGrid(context, videoIntroController),
// 合集 // 合集
if (!loadingStatus && if (!loadingStatus &&
widget.videoDetail!.ugcSeason != null) ...[ widget.videoDetail!.ugcSeason != null) ...[
@ -458,7 +458,7 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
Widget actionGrid(BuildContext context, videoIntroController) { Widget actionGrid(BuildContext context, videoIntroController) {
return LayoutBuilder(builder: (context, constraints) { return LayoutBuilder(builder: (context, constraints) {
return Container( return Container(
padding: const EdgeInsets.only(top: 6, bottom: 10), margin: const EdgeInsets.only(top: 6, bottom: 4),
height: constraints.maxWidth / 5 * 0.8, height: constraints.maxWidth / 5 * 0.8,
child: GridView.count( child: GridView.count(
primary: false, primary: false,
@ -477,12 +477,12 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
? widget.videoDetail!.stat!.like!.toString() ? widget.videoDetail!.stat!.like!.toString()
: '-'), : '-'),
), ),
ActionItem( // ActionItem(
icon: const Icon(FontAwesomeIcons.clock), // icon: const Icon(FontAwesomeIcons.clock),
onTap: () => videoIntroController.actionShareVideo(), // onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false, // selectStatus: false,
loadingStatus: loadingStatus, // loadingStatus: loadingStatus,
text: '稍后再看'), // text: '稍后再看'),
Obx( Obx(
() => ActionItem( () => ActionItem(
icon: const Icon(FontAwesomeIcons.b), icon: const Icon(FontAwesomeIcons.b),
@ -498,22 +498,28 @@ class _VideoInfoState extends State<VideoInfo> with TickerProviderStateMixin {
() => ActionItem( () => ActionItem(
icon: const Icon(FontAwesomeIcons.star), icon: const Icon(FontAwesomeIcons.star),
selectIcon: const Icon(FontAwesomeIcons.solidStar), selectIcon: const Icon(FontAwesomeIcons.solidStar),
// onTap: () => videoIntroController.actionFavVideo(),
onTap: () => showFavBottomSheet(), onTap: () => showFavBottomSheet(),
onLongPress: () => showFavBottomSheet(type: 'longPress'),
selectStatus: videoIntroController.hasFav.value, selectStatus: videoIntroController.hasFav.value,
loadingStatus: loadingStatus, loadingStatus: loadingStatus,
text: !loadingStatus text: !loadingStatus
? widget.videoDetail!.stat!.favorite!.toString() ? widget.videoDetail!.stat!.favorite!.toString()
: '-'), : '-'),
), ),
ActionItem(
icon: const Icon(FontAwesomeIcons.comment),
onTap: () => videoDetailCtr.tabCtr.animateTo(1),
selectStatus: false,
loadingStatus: loadingStatus,
text: !loadingStatus
? widget.videoDetail!.stat!.reply!.toString()
: '评论'),
ActionItem( ActionItem(
icon: const Icon(FontAwesomeIcons.shareFromSquare), icon: const Icon(FontAwesomeIcons.shareFromSquare),
onTap: () => videoIntroController.actionShareVideo(), onTap: () => videoIntroController.actionShareVideo(),
selectStatus: false, selectStatus: false,
loadingStatus: loadingStatus, loadingStatus: loadingStatus,
text: !loadingStatus text: '分享'),
? widget.videoDetail!.stat!.share!.toString()
: '-'),
], ],
), ),
); );

View File

@ -6,6 +6,7 @@ class ActionItem extends StatelessWidget {
final Icon? icon; final Icon? icon;
final Icon? selectIcon; final Icon? selectIcon;
final Function? onTap; final Function? onTap;
final Function? onLongPress;
final bool? loadingStatus; final bool? loadingStatus;
final String? text; final String? text;
final bool selectStatus; final bool selectStatus;
@ -15,6 +16,7 @@ class ActionItem extends StatelessWidget {
this.icon, this.icon,
this.selectIcon, this.selectIcon,
this.onTap, this.onTap,
this.onLongPress,
this.loadingStatus, this.loadingStatus,
this.text, this.text,
this.selectStatus = false, this.selectStatus = false,
@ -27,6 +29,9 @@ class ActionItem extends StatelessWidget {
feedBack(), feedBack(),
onTap!(), onTap!(),
}, },
onLongPress: () => {
if (onLongPress != null) {onLongPress!()}
},
borderRadius: StyleString.mdRadius, borderRadius: StyleString.mdRadius,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,

View File

@ -0,0 +1,156 @@
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/common/widgets/http_error.dart';
import 'package:pilipala/http/member.dart';
import 'package:pilipala/models/member/tags.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart';
class GroupPanel extends StatefulWidget {
final int? mid;
const GroupPanel({super.key, this.mid});
@override
State<GroupPanel> createState() => _GroupPanelState();
}
class _GroupPanelState extends State<GroupPanel> {
Box localCache = GStrorage.localCache;
late double sheetHeight;
late Future _futureBuilderFuture;
late List<MemberTagItemModel> tagsList;
bool showDefault = true;
@override
void initState() {
super.initState();
sheetHeight = localCache.get('sheetHeight');
_futureBuilderFuture = MemberHttp.followUpTags();
}
void onSave() async {
feedBack();
// 是否有选中的 有选中的带id没选使用默认0
bool anyHasChecked = tagsList.any((e) => e.checked == true);
late String tagids;
if (anyHasChecked) {
List checkedList = tagsList.where((e) => e.checked == true).toList();
List<int> tagidList = checkedList.map<int>((e) => e.tagid).toList();
tagids = tagidList.join(',');
} else {
tagids = '0';
}
// 保存
var res = await MemberHttp.addUsers(widget.mid, tagids);
SmartDialog.showToast(res['msg']);
if (res['status']) {
Get.back();
}
}
@override
Widget build(BuildContext context) {
return Container(
height: sheetHeight,
color: Theme.of(context).colorScheme.background,
child: Column(
children: [
AppBar(
centerTitle: false,
elevation: 0,
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.close_outlined)),
title:
Text('设置关注分组', style: Theme.of(context).textTheme.titleMedium),
),
Expanded(
child: Material(
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
tagsList = data['data'];
return ListView.builder(
itemCount: data['data'].length,
itemBuilder: (context, index) {
return ListTile(
onTap: () {
data['data'][index].checked =
!data['data'][index].checked;
showDefault =
!data['data'].any((e) => e.checked == true);
setState(() {});
},
dense: true,
leading: const Icon(Icons.group_outlined),
minLeadingWidth: 0,
title: Text(data['data'][index].name),
subtitle: data['data'][index].tip != ''
? Text(data['data'][index].tip)
: null,
trailing: Transform.scale(
scale: 0.9,
child: Checkbox(
value: data['data'][index].checked,
onChanged: (bool? checkValue) {
data['data'][index].checked = checkValue;
showDefault = !data['data']
.any((e) => e.checked == true);
setState(() {});
},
),
),
);
},
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
);
}
} else {
// 骨架屏
return const Text('请求中');
}
},
),
),
),
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: () => onSave(),
style: TextButton.styleFrom(
padding: const EdgeInsets.only(left: 30, right: 30),
foregroundColor: Theme.of(context).colorScheme.onPrimary,
backgroundColor:
Theme.of(context).colorScheme.primary, // 设置按钮背景色
),
child: Text(showDefault ? '保存至默认分组' : '保存'),
),
],
),
),
],
),
);
}
}

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/models/video_detail_res.dart'; import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/index.dart';
class PagesPanel extends StatefulWidget { class PagesPanel extends StatefulWidget {
final List<Part> pages; final List<Part> pages;
@ -22,13 +23,23 @@ class PagesPanel extends StatefulWidget {
class _PagesPanelState extends State<PagesPanel> { class _PagesPanelState extends State<PagesPanel> {
late List<Part> episodes; late List<Part> episodes;
late int cid;
late int currentIndex; late int currentIndex;
String heroTag = Get.arguments['heroTag'];
late VideoDetailController _videoDetailController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
cid = widget.cid!;
episodes = widget.pages; episodes = widget.pages;
currentIndex = episodes.indexWhere((e) => e.cid == widget.cid); _videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
currentIndex = episodes.indexWhere((e) => e.cid == cid);
_videoDetailController.cid.listen((p0) {
cid = p0;
setState(() {});
currentIndex = episodes.indexWhere((e) => e.cid == cid);
});
} }
void changeFucCall(item, i) async { void changeFucCall(item, i) async {

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:pilipala/models/video_detail_res.dart'; import 'package:pilipala/models/video_detail_res.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/utils/id_utils.dart'; import 'package:pilipala/utils/id_utils.dart';
class SeasonPanel extends StatefulWidget { class SeasonPanel extends StatefulWidget {
@ -23,11 +24,16 @@ class SeasonPanel extends StatefulWidget {
class _SeasonPanelState extends State<SeasonPanel> { class _SeasonPanelState extends State<SeasonPanel> {
late List<EpisodeItem> episodes; late List<EpisodeItem> episodes;
late int cid;
late int currentIndex; late int currentIndex;
String heroTag = Get.arguments['heroTag'];
late VideoDetailController _videoDetailController;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
cid = widget.cid!;
_videoDetailController = Get.find<VideoDetailController>(tag: heroTag);
/// 根据 cid 找到对应集,找到对应 episodes /// 根据 cid 找到对应集,找到对应 episodes
/// 有多个episodes时只显示其中一个 /// 有多个episodes时只显示其中一个
@ -36,7 +42,7 @@ class _SeasonPanelState extends State<SeasonPanel> {
for (int i = 0; i < sections.length; i++) { for (int i = 0; i < sections.length; i++) {
List<EpisodeItem> episodesList = sections[i].episodes!; List<EpisodeItem> episodesList = sections[i].episodes!;
for (int j = 0; j < episodesList.length; j++) { for (int j = 0; j < episodesList.length; j++) {
if (episodesList[j].cid == widget.cid) { if (episodesList[j].cid == cid) {
episodes = episodesList; episodes = episodesList;
continue; continue;
} }
@ -47,7 +53,12 @@ class _SeasonPanelState extends State<SeasonPanel> {
// episodes = widget.ugcSeason.sections! // episodes = widget.ugcSeason.sections!
// .firstWhere((e) => e.seasonId == widget.ugcSeason.id) // .firstWhere((e) => e.seasonId == widget.ugcSeason.id)
// .episodes!; // .episodes!;
currentIndex = episodes.indexWhere((e) => e.cid == widget.cid); currentIndex = episodes.indexWhere((e) => e.cid == cid);
_videoDetailController.cid.listen((p0) {
cid = p0;
setState(() {});
currentIndex = episodes.indexWhere((e) => e.cid == cid);
});
} }
void changeFucCall(item, i) async { void changeFucCall(item, i) async {
@ -57,6 +68,7 @@ class _SeasonPanelState extends State<SeasonPanel> {
item.aid, item.aid,
); );
currentIndex = i; currentIndex = i;
setState(() {});
Get.back(); Get.back();
} }

View File

@ -20,6 +20,7 @@ import 'package:pilipala/pages/video/detail/controller.dart';
import 'package:pilipala/pages/video/detail/introduction/index.dart'; import 'package:pilipala/pages/video/detail/introduction/index.dart';
import 'package:pilipala/pages/video/detail/related/index.dart'; import 'package:pilipala/pages/video/detail/related/index.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'widgets/app_bar.dart'; import 'widgets/app_bar.dart';
@ -41,6 +42,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
final ScrollController _extendNestCtr = ScrollController(); final ScrollController _extendNestCtr = ScrollController();
late StreamController<double> appbarStream; late StreamController<double> appbarStream;
late VideoIntroController videoIntroController; late VideoIntroController videoIntroController;
late BangumiIntroController bangumiIntroController;
late String heroTag; late String heroTag;
PlayerStatus playerStatus = PlayerStatus.playing; PlayerStatus playerStatus = PlayerStatus.playing;
@ -61,6 +63,7 @@ class _VideoDetailPageState extends State<VideoDetailPage>
heroTag = Get.arguments['heroTag']; heroTag = Get.arguments['heroTag'];
videoDetailController = Get.put(VideoDetailController(), tag: heroTag); videoDetailController = Get.put(VideoDetailController(), tag: heroTag);
videoIntroController = Get.put(VideoIntroController(), tag: heroTag); videoIntroController = Get.put(VideoIntroController(), tag: heroTag);
bangumiIntroController = Get.put(BangumiIntroController(), tag: heroTag);
statusBarHeight = localCache.get('statusBarHeight'); statusBarHeight = localCache.get('statusBarHeight');
autoExitFullcreen = autoExitFullcreen =
setting.get(SettingBoxKey.enableAutoExit, defaultValue: false); setting.get(SettingBoxKey.enableAutoExit, defaultValue: false);
@ -98,6 +101,23 @@ class _VideoDetailPageState extends State<VideoDetailPage>
if (autoExitFullcreen) { if (autoExitFullcreen) {
plPlayerController!.triggerFullScreen(status: false); plPlayerController!.triggerFullScreen(status: false);
} }
/// 顺序播放 列表循环
if (plPlayerController!.playRepeat != PlayRepeat.pause &&
plPlayerController!.playRepeat != PlayRepeat.singleCycle) {
if (videoDetailController.videoType == SearchType.video) {
videoIntroController.nextPlay();
}
if (videoDetailController.videoType == SearchType.media_bangumi) {
bangumiIntroController.nextPlay();
}
}
/// 单个循环
if (plPlayerController!.playRepeat == PlayRepeat.singleCycle) {
plPlayerController!.seekTo(Duration.zero);
plPlayerController!.play();
}
// 播放完展示控制栏 // 播放完展示控制栏
try { try {
PiPStatus currentStatus = PiPStatus currentStatus =
@ -385,8 +405,8 @@ class _VideoDetailPageState extends State<VideoDetailPage>
const VideoIntroPanel(), const VideoIntroPanel(),
] else if (videoDetailController.videoType == ] else if (videoDetailController.videoType ==
SearchType.media_bangumi) ...[ SearchType.media_bangumi) ...[
BangumiIntroPanel( Obx(() => BangumiIntroPanel(
cid: videoDetailController.cid) cid: videoDetailController.cid.value)),
], ],
// if (videoDetailController.videoType == // if (videoDetailController.videoType ==
// SearchType.video) ...[ // SearchType.video) ...[

View File

@ -13,6 +13,7 @@ import 'package:pilipala/models/video/play/url.dart';
import 'package:pilipala/pages/video/detail/index.dart'; import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart'; import 'package:pilipala/pages/video/detail/introduction/widgets/menu_row.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
class HeaderControl extends StatefulWidget implements PreferredSizeWidget { class HeaderControl extends StatefulWidget implements PreferredSizeWidget {
@ -56,7 +57,7 @@ class _HeaderControlState extends State<HeaderControl> {
builder: (_) { builder: (_) {
return Container( return Container(
width: double.infinity, width: double.infinity,
height: 400, height: 440,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background, color: Theme.of(context).colorScheme.background,
@ -149,13 +150,14 @@ class _HeaderControlState extends State<HeaderControl> {
'当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}', '当前解码格式 ${widget.videoDetailCtr!.currentDecodeFormats.description}',
style: subTitleStyle), style: subTitleStyle),
), ),
// ListTile( ListTile(
// onTap: () {}, onTap: () => {Get.back(), showSetRepeat()},
// dense: true, dense: true,
// enabled: false, leading: const Icon(Icons.repeat, size: 20),
// leading: const Icon(Icons.play_circle_outline, size: 20), title: Text('播放顺序', style: titleStyle),
// title: Text('播放设置', style: titleStyle), subtitle: Text(widget.controller!.playRepeat.description,
// ), style: subTitleStyle),
),
ListTile( ListTile(
onTap: () => {Get.back(), showSetDanmaku()}, onTap: () => {Get.back(), showSetDanmaku()},
dense: true, dense: true,
@ -704,6 +706,60 @@ class _HeaderControlState extends State<HeaderControl> {
); );
} }
/// 播放顺序
void showSetRepeat() async {
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 PlayRepeat.values) ...[
ListTile(
onTap: () {
widget.controller!.setPlayRepeat(i);
Get.back();
},
dense: true,
contentPadding:
const EdgeInsets.only(left: 20, right: 20),
title: Text(i.description),
trailing: widget.controller!.playRepeat == i
? Icon(
Icons.done,
color: Theme.of(context).colorScheme.primary,
)
: const SizedBox(),
)
],
],
),
),
),
],
),
);
},
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final _ = widget.controller!; final _ = widget.controller!;

View File

@ -13,6 +13,7 @@ import 'package:media_kit_video/media_kit_video.dart';
import 'package:ns_danmaku/ns_danmaku.dart'; import 'package:ns_danmaku/ns_danmaku.dart';
import 'package:pilipala/http/video.dart'; import 'package:pilipala/http/video.dart';
import 'package:pilipala/plugin/pl_player/index.dart'; import 'package:pilipala/plugin/pl_player/index.dart';
import 'package:pilipala/plugin/pl_player/models/play_repeat.dart';
import 'package:pilipala/utils/feed_back.dart'; import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/storage.dart'; import 'package:pilipala/utils/storage.dart';
import 'package:screen_brightness/screen_brightness.dart'; import 'package:screen_brightness/screen_brightness.dart';
@ -209,6 +210,9 @@ class PlPlayerController {
late double fontSizeVal; late double fontSizeVal;
late double danmakuSpeedVal; late double danmakuSpeedVal;
// 播放顺序相关
PlayRepeat playRepeat = PlayRepeat.pause;
// 添加一个私有构造函数 // 添加一个私有构造函数
PlPlayerController._() { PlPlayerController._() {
_videoType = videoType; _videoType = videoType;
@ -226,6 +230,12 @@ class PlPlayerController {
// 弹幕速度 // 弹幕速度
danmakuSpeedVal = danmakuSpeedVal =
localCache.get(LocalCacheKey.danmakuSpeed, defaultValue: 4.0); localCache.get(LocalCacheKey.danmakuSpeed, defaultValue: 4.0);
playRepeat = PlayRepeat.values.toList().firstWhere(
(e) =>
e.value ==
videoStorage.get(VideoBoxKey.playRepeat,
defaultValue: PlayRepeat.pause.value),
);
// _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) { // _playerEventSubs = onPlayerStatusChanged.listen((PlayerStatus status) {
// if (status == PlayerStatus.playing) { // if (status == PlayerStatus.playing) {
// WakelockPlus.enable(); // WakelockPlus.enable();
@ -910,6 +920,11 @@ class PlPlayerController {
} }
} }
setPlayRepeat(PlayRepeat type) {
playRepeat = type;
videoStorage.put(VideoBoxKey.playRepeat, type.value);
}
Future<void> dispose({String type = 'single'}) async { Future<void> dispose({String type = 'single'}) async {
// 每次减1最后销毁 // 每次减1最后销毁
if (type == 'single' && playerCount.value > 1) { if (type == 'single' && playerCount.value > 1) {

View File

@ -0,0 +1,25 @@
enum PlayRepeat {
pause,
listOrder,
singleCycle,
listCycle,
}
extension PlayRepeatExtension on PlayRepeat {
static final List<String> _descList = [
'播完暂停',
'顺序播放',
'单个循环',
'列表循环',
];
get description => _descList[index];
static final List<double> _valueList = [
1,
2,
3,
4,
];
get value => _valueList[index];
get defaultValue => _valueList[1];
}

View File

@ -158,4 +158,6 @@ class VideoBoxKey {
static const String videoBrightness = 'videoBrightness'; static const String videoBrightness = 'videoBrightness';
// 倍速 // 倍速
static const String videoSpeed = 'videoSpeed'; static const String videoSpeed = 'videoSpeed';
// 播放顺序
static const String playRepeat = 'playRepeat';
} }