feat: 动态筛选&UP主筛选
This commit is contained in:
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -73,6 +73,9 @@ class DynamicDetailController extends GetxController {
|
||||
} else {
|
||||
replyList.addAll(replies);
|
||||
}
|
||||
if (replyList.length == acount.value) {
|
||||
noMore.value = '没有更多了';
|
||||
}
|
||||
}
|
||||
isLoadingMore = false;
|
||||
return res;
|
||||
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
263
lib/pages/dynamics/widgets/up_panel.dart
Normal file
263
lib/pages/dynamics/widgets/up_panel.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
)),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user