feat: 动态筛选&UP主筛选

This commit is contained in:
guozhigq
2023-06-29 23:37:55 +08:00
parent eca48bc77e
commit b43b9549b9
9 changed files with 557 additions and 71 deletions

View File

@ -134,6 +134,7 @@ class Api {
// 正在直播的up & 关注的up
// https://api.bilibili.com/x/polymer/web-dynamic/v1/portal
static const String followUp = '/x/polymer/web-dynamic/v1/portal';
// 关注的up动态
// https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/all

View File

@ -1,19 +1,26 @@
import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/dynamics/up.dart';
class DynamicsHttp {
static Future followDynamic({
String? type,
int? page,
String? offset,
int? mid,
}) async {
var res = await Request().get(Api.followDynamic, data: {
Map<String, dynamic> data = {
'type': type ?? 'all',
'page': page ?? 1,
'timezone_offset': '-480',
'offset': page == 1 ? '' : offset,
'features': 'itemOpusStyle'
});
};
if (mid != -1) {
data['host_mid'] = mid;
data.remove('timezone_offset');
}
var res = await Request().get(Api.followDynamic, data: data);
if (res.data['code'] == 0) {
return {
'status': true,
@ -27,4 +34,20 @@ class DynamicsHttp {
};
}
}
static Future followUp() async {
var res = await Request().get(Api.followUp);
if (res.data['code'] == 0) {
return {
'status': true,
'data': FollowUpModel.fromJson(res.data['data']),
};
} else {
return {
'status': false,
'data': [],
'msg': res.data['message'],
};
}
}
}

View File

@ -7,5 +7,5 @@ enum DynamicsType {
extension BusinessTypeExtension on DynamicsType {
String get values => ['all', 'video', 'pgc', 'article'][index];
String get labels => ['全部', '视频投稿', '追番追剧', '专栏'][index];
String get labels => ['全部', '视频', '追番', '专栏'][index];
}

View File

@ -526,7 +526,7 @@ class OpusPicsModel {
OpusPicsModel.fromJson(Map<String, dynamic> json) {
width = json['width'];
height = json['height'];
size = json['size'].toInt();
size = json['size'] != null ? json['size'].toInt() : 0;
src = json['src'];
url = json['url'];
}

View File

@ -0,0 +1,93 @@
class FollowUpModel {
FollowUpModel({
this.liveUsers,
this.upList,
});
LiveUsers? liveUsers;
List<UpItem>? upList;
FollowUpModel.fromJson(Map<String, dynamic> json) {
liveUsers = LiveUsers.fromJson(json['live_users']);
upList = json['up_list'] != null
? json['up_list'].map<UpItem>((e) => UpItem.fromJson(e)).toList()
: [];
}
}
class LiveUsers {
LiveUsers({
this.count,
this.group,
this.items,
});
int? count;
String? group;
List<LiveUserItem>? items;
LiveUsers.fromJson(Map<String, dynamic> json) {
count = json['count'];
group = json['group'];
items = json['items']
.map<LiveUserItem>((e) => LiveUserItem.fromJson(e))
.toList();
}
}
class LiveUserItem {
LiveUserItem({
this.face,
this.isReserveRecall,
this.jumpUrl,
this.mid,
this.roomId,
this.title,
this.uname,
});
String? face;
bool? isReserveRecall;
String? jumpUrl;
int? mid;
int? roomId;
String? title;
String? uname;
bool hasUpdate = false;
String type = 'live';
LiveUserItem.fromJson(Map<String, dynamic> json) {
face = json['face'];
isReserveRecall = json['is_reserve_recall'];
jumpUrl = json['jump_url'];
mid = json['mid'];
roomId = json['room_id'];
title = json['title'];
uname = json['uname'];
}
}
class UpItem {
UpItem({
this.face,
this.hasUpdate,
this.isReserveRecall,
this.mid,
this.uname,
});
String? face;
bool? hasUpdate;
bool? isReserveRecall;
int? mid;
String? uname;
String type = 'up';
UpItem.fromJson(Map<String, dynamic> json) {
face = json['face'];
hasUpdate = json['has_update'];
isReserveRecall = json['is_reserve_recall'];
mid = json['mid'];
uname = json['uname'];
}
}

View File

@ -3,22 +3,53 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/http/dynamics.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/common/dynamics_type.dart';
import 'package:pilipala/models/dynamics/result.dart';
import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/utils/utils.dart';
class DynamicsController extends GetxController {
int page = 1;
String? offset = '';
RxList<DynamicItemModel>? dynamicsList = [DynamicItemModel()].obs;
RxString dynamicsType = 'all'.obs;
Rx<DynamicsType> dynamicsType = DynamicsType.values[0].obs;
RxString dynamicsTypeLabel = '全部'.obs;
final ScrollController scrollController = ScrollController();
Rx<FollowUpModel> upData = FollowUpModel().obs;
// 默认获取全部动态
int mid = -1;
List filterTypeList = [
{
'label': DynamicsType.all.labels,
'value': DynamicsType.all,
'enabled': true
},
{
'label': DynamicsType.video.labels,
'value': DynamicsType.video,
'enabled': true
},
{
'label': DynamicsType.pgc.labels,
'value': DynamicsType.pgc,
'enabled': true
},
{
'label': DynamicsType.article.labels,
'value': DynamicsType.article,
'enabled': true
},
];
Future queryFollowDynamic({type = 'init'}) async {
// if (type == 'init') {
// dynamicsList!.value = [];
// }
var res = await DynamicsHttp.followDynamic(
page: type == 'init' ? 1 : page,
type: dynamicsType.value,
type: dynamicsType.value.values,
offset: offset,
mid: mid,
);
if (res['status']) {
if (type == 'init') {
@ -32,12 +63,10 @@ class DynamicsController extends GetxController {
return res;
}
onSelectType(value, label) async {
onSelectType(value) async {
dynamicsType.value = value;
dynamicsTypeLabel.value = label;
await queryFollowDynamic();
scrollController.animateTo(0,
duration: const Duration(milliseconds: 500), curve: Curves.easeInOut);
scrollController.jumpTo(0);
}
pushDetail(item, floor, {action = 'all'}) async {
@ -86,4 +115,18 @@ class DynamicsController extends GetxController {
break;
}
}
Future queryFollowUp() async {
var res = await DynamicsHttp.followUp();
if (res['status']) {
upData.value = res['data'];
}
return res;
}
onSelectUp(mid) async {
dynamicsType.value = DynamicsType.values[0];
queryFollowDynamic();
}
}

View File

@ -73,6 +73,9 @@ class DynamicDetailController extends GetxController {
} else {
replyList.addAll(replies);
}
if (replyList.length == acount.value) {
noMore.value = '没有更多了';
}
}
isLoadingMore = false;
return res;

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/skeleton/dynamic_card.dart';
import 'package:pilipala/common/widgets/http_error.dart';
@ -7,6 +8,7 @@ import 'package:pilipala/models/dynamics/result.dart';
import 'controller.dart';
import 'widgets/dynamic_panel.dart';
import 'widgets/up_panel.dart';
class DynamicsPage extends StatefulWidget {
const DynamicsPage({super.key});
@ -19,7 +21,6 @@ class _DynamicsPageState extends State<DynamicsPage>
with AutomaticKeepAliveClientMixin {
final DynamicsController _dynamicsController = Get.put(DynamicsController());
Future? _futureBuilderFuture;
// final ScrollController scrollController = ScrollController();
bool _isLoadingMore = false;
@override
bool get wantKeepAlive => true;
@ -48,81 +49,140 @@ class _DynamicsPageState extends State<DynamicsPage>
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
centerTitle: false,
title: const Text('动态'),
actions: [
Obx(
() => PopupMenuButton(
initialValue: _dynamicsController.dynamicsType.value,
position: PopupMenuPosition.under,
itemBuilder: (context) => [
for (var i in DynamicsType.values) ...[
PopupMenuItem(
value: i.values,
onTap: () =>
_dynamicsController.onSelectType(i.values, i.labels),
child: Text(i.labels),
)
],
],
child: Row(
elevation: 0,
scrolledUnderElevation: 0,
titleSpacing: 0,
title: SizedBox(
height: 36,
child: Stack(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
_dynamicsController.dynamicsTypeLabel.value,
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
Obx(
() => SegmentedButton<DynamicsType>(
showSelectedIcon: false,
style: ButtonStyle(
padding: MaterialStateProperty.all(
const EdgeInsets.symmetric(
vertical: 0, horizontal: 10)),
side: MaterialStateProperty.all(
BorderSide(
color: Theme.of(context).hintColor, width: 0.5),
),
),
segments: <ButtonSegment<DynamicsType>>[
for (var i in _dynamicsController.filterTypeList) ...[
ButtonSegment<DynamicsType>(
value: i['value'],
label: Padding(
padding: const EdgeInsets.only(bottom: 10),
child: Text(i['label']),
),
enabled: i['enabled'],
),
]
],
selected: <DynamicsType>{
_dynamicsController.dynamicsType.value
},
onSelectionChanged: (Set<DynamicsType> newSelection) {
_dynamicsController.dynamicsType.value =
newSelection.first;
_dynamicsController.onSelectType(newSelection.first);
},
),
),
const SizedBox(width: 10)
],
),
),
Positioned(
right: 10,
top: 0,
bottom: 0,
child: IconButton(
padding: EdgeInsets.zero,
onPressed: () {
_dynamicsController.mid = -1;
_dynamicsController.dynamicsType.value =
DynamicsType.values[0];
SmartDialog.showToast('还原默认加载',
alignment: Alignment.topCenter);
_dynamicsController.queryFollowDynamic();
},
icon: const Icon(Icons.history),
),
)
],
),
const SizedBox(width: 4)
],
),
),
body: RefreshIndicator(
onRefresh: () async {
_dynamicsController.page = 1;
_dynamicsController.queryFollowUp();
await _dynamicsController.queryFollowDynamic();
},
child: FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data;
if (data['status']) {
List<DynamicItemModel> list = _dynamicsController.dynamicsList!;
return Obx(
() => ListView.builder(
controller: _dynamicsController.scrollController,
shrinkWrap: true,
itemCount: list.length,
itemBuilder: (BuildContext context, index) {
return DynamicPanel(item: list[index]);
},
),
);
} else {
return CustomScrollView(
slivers: [
HttpError(
child: CustomScrollView(
controller: _dynamicsController.scrollController,
slivers: [
FutureBuilder(
future: _dynamicsController.queryFollowUp(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data;
if (data['status']) {
return Obx(() => UpPanel(_dynamicsController.upData.value));
} else {
return const SliverToBoxAdapter(
child: SizedBox(height: 80));
}
} else {
return const SliverToBoxAdapter(
child: SizedBox(
height: 115,
child: UpPanelSkeleton(),
));
}
},
),
FutureBuilder(
future: _futureBuilderFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data;
if (data['status']) {
List<DynamicItemModel> list =
_dynamicsController.dynamicsList!;
return Obx(
() => SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return DynamicPanel(item: list[index]);
}, childCount: list.length),
),
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () => _dynamicsController.queryFollowDynamic(),
)
],
);
}
} else {
// 骨架屏
return ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemCount: 5,
itemBuilder: ((context, index) => const DynamicCardSkeleton()),
);
}
},
);
}
} else {
// 骨架屏
return skeleton();
}
},
),
],
),
),
);
}
Widget skeleton() {
return SliverList(
delegate: SliverChildBuilderDelegate((context, index) {
return const DynamicCardSkeleton();
}, childCount: 5),
);
}
}

View File

@ -0,0 +1,263 @@
import 'package:flutter/material.dart';
import 'package:flutter_meedu_media_kit/meedu_player.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.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/dynamics_type.dart';
import 'package:pilipala/models/dynamics/up.dart';
import 'package:pilipala/pages/dynamics/controller.dart';
class UpPanel extends StatefulWidget {
FollowUpModel? upData;
UpPanel(this.upData, {Key? key}) : super(key: key);
@override
State<UpPanel> createState() => _UpPanelState();
}
class _UpPanelState extends State<UpPanel> {
final ScrollController scrollController = ScrollController();
int currentMid = -1;
late double contentWidth = 56;
List<UpItem> upList = [];
List<LiveUserItem> liveList = [];
static const itemPadding = EdgeInsets.symmetric(horizontal: 5, vertical: 0);
@override
void initState() {
super.initState();
upList = widget.upData!.upList!;
liveList = widget.upData!.liveUsers!.items!;
upList.insert(0, UpItem(face: '', uname: '全部动态', mid: -1));
}
@override
Widget build(BuildContext context) {
return SliverPersistentHeader(
floating: true,
pinned: false,
delegate: _SliverHeaderDelegate(
height: 115,
child: Container(
height: 115,
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
color: Theme.of(context).dividerColor.withOpacity(0.1),
blurRadius: 10,
spreadRadius: 2,
),
],
color: Theme.of(context).colorScheme.surface,
),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(
top: 5, left: 12, right: 12, bottom: 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text(
'最常访问',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
Expanded(
child: ListView(
scrollDirection: Axis.horizontal,
controller: scrollController,
children: [
const SizedBox(width: 10),
for (int i = 0; i < liveList.length; i++) ...[
upItemBuild(liveList[i], i)
],
VerticalDivider(
indent: 15,
endIndent: 35,
width: 26,
color: Theme.of(context).primaryColor.withOpacity(0.5),
),
for (int i = 0; i < upList.length; i++) ...[
upItemBuild(upList[i], i)
],
const SizedBox(width: 10),
],
),
)
],
),
),
),
);
}
Widget upItemBuild(data, i) {
bool isCurrent = currentMid == data.mid || currentMid == -1;
return InkWell(
onTap: () {
if (data.type == 'up') {
currentMid = data.mid;
Get.find<DynamicsController>().mid = data.mid;
Get.find<DynamicsController>().onSelectUp(data.mid);
int liveLen = liveList.length;
int upLen = upList.length;
double itemWidth = contentWidth + itemPadding.horizontal;
double screenWidth = MediaQuery.of(context).size.width;
double moveDistance = 0.0;
if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
moveDistance =
(i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
} else {
moveDistance = (upLen + liveLen) * itemWidth + 46 - screenWidth;
}
scrollController.animateTo(
moveDistance,
duration: const Duration(milliseconds: 500),
curve: Curves.easeInOut,
);
setState(() {});
} else if (data.type == 'live') {
SmartDialog.showToast('直播功能暂未开发');
}
},
child: Padding(
padding: itemPadding,
child: AnimatedOpacity(
opacity: isCurrent ? 1 : 0.3,
duration: const Duration(milliseconds: 200),
curve: Curves.easeInOut,
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Badge(
smallSize: 8,
label: data.type == 'live' ? const Text('Live') : null,
textColor: Theme.of(context).colorScheme.onPrimary,
alignment: AlignmentDirectional.bottomCenter,
padding: const EdgeInsets.only(left: 4, right: 4),
isLabelVisible: data.type == 'live' ||
(data.type == 'up' && (data.hasUpdate ?? false)),
backgroundColor: Theme.of(context).primaryColor,
child: NetworkImgLayer(
width: 49,
height: 49,
src: data.face,
type: 'avatar',
),
),
Padding(
padding: const EdgeInsets.only(top: 4),
child: SizedBox(
width: contentWidth,
child: Text(
data.uname,
overflow: TextOverflow.fade,
softWrap: false,
textAlign: TextAlign.center,
style: TextStyle(
color: currentMid == data.mid
? Theme.of(context).colorScheme.primary
: Theme.of(context).colorScheme.outline,
fontSize:
Theme.of(context).textTheme.labelMedium!.fontSize),
),
),
),
],
),
),
),
);
}
}
class _SliverHeaderDelegate extends SliverPersistentHeaderDelegate {
_SliverHeaderDelegate({required this.height, required this.child});
final double height;
final Widget child;
@override
Widget build(
BuildContext context, double shrinkOffset, bool overlapsContent) {
return child;
}
@override
double get maxExtent => height;
@override
double get minExtent => height;
@override
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
true;
}
class UpPanelSkeleton extends StatelessWidget {
const UpPanelSkeleton({super.key});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding:
const EdgeInsets.only(top: 5, left: 12, right: 12, bottom: 5),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: const [
Text(
'最常访问',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
),
),
Expanded(
child: ListView.builder(
scrollDirection: Axis.horizontal,
physics: const NeverScrollableScrollPhysics(),
itemCount: 10,
itemBuilder: ((context, index) => Padding(
padding:
const EdgeInsets.symmetric(horizontal: 10, vertical: 0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 49,
height: 49,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.onInverseSurface,
borderRadius: const BorderRadius.all(
Radius.circular(24),
),
),
),
Container(
margin: const EdgeInsets.only(top: 6),
width: 45,
height: 12,
color: Theme.of(context).colorScheme.onInverseSurface,
),
],
),
)),
),
)
],
);
}
}