feat: 轮播查看up动态
This commit is contained in:
@ -6,9 +6,7 @@ import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/http/dynamics.dart';
|
||||
import 'package:pilipala/http/search.dart';
|
||||
import 'package:pilipala/models/bangumi/info.dart';
|
||||
import 'package:pilipala/models/common/dynamics_type.dart';
|
||||
import 'package:pilipala/models/common/search_type.dart';
|
||||
import 'package:pilipala/models/dynamics/result.dart';
|
||||
import 'package:pilipala/models/dynamics/up.dart';
|
||||
import 'package:pilipala/models/live/item.dart';
|
||||
@ -16,7 +14,6 @@ import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/id_utils.dart';
|
||||
import 'package:pilipala/utils/route_push.dart';
|
||||
import 'package:pilipala/utils/storage.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
class DynamicsController extends GetxController {
|
||||
int page = 1;
|
||||
@ -282,4 +279,11 @@ class DynamicsController extends GetxController {
|
||||
dynamicsList.value = <DynamicItemModel>[];
|
||||
queryFollowDynamic();
|
||||
}
|
||||
|
||||
// 点击up主
|
||||
void onTapUp(data) {
|
||||
mid.value = data.mid;
|
||||
upInfo.value = data;
|
||||
onSelectUp(data.mid);
|
||||
}
|
||||
}
|
||||
|
46
lib/pages/dynamics/up_dynamic/controller.dart
Normal file
46
lib/pages/dynamics/up_dynamic/controller.dart
Normal file
@ -0,0 +1,46 @@
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/http/dynamics.dart';
|
||||
import 'package:pilipala/models/dynamics/result.dart';
|
||||
import 'package:pilipala/models/dynamics/up.dart';
|
||||
|
||||
class UpDynamicsController extends GetxController {
|
||||
UpDynamicsController(this.upInfo);
|
||||
UpItem upInfo;
|
||||
RxList<DynamicItemModel> dynamicsList = <DynamicItemModel>[].obs;
|
||||
RxBool isLoadingDynamic = false.obs;
|
||||
String? offset = '';
|
||||
int page = 1;
|
||||
|
||||
Future queryFollowDynamic({type = 'init'}) async {
|
||||
if (type == 'init') {
|
||||
dynamicsList.clear();
|
||||
}
|
||||
// 下拉刷新数据渲染时会触发onLoad
|
||||
if (type == 'onLoad' && page == 1) {
|
||||
return;
|
||||
}
|
||||
isLoadingDynamic.value = true;
|
||||
var res = await DynamicsHttp.followDynamic(
|
||||
page: type == 'init' ? 1 : page,
|
||||
type: 'all',
|
||||
offset: offset,
|
||||
mid: upInfo.mid,
|
||||
);
|
||||
isLoadingDynamic.value = false;
|
||||
if (res['status']) {
|
||||
if (type == 'onLoad' && res['data'].items.isEmpty) {
|
||||
SmartDialog.showToast('没有更多了');
|
||||
return;
|
||||
}
|
||||
if (type == 'init') {
|
||||
dynamicsList.value = res['data'].items;
|
||||
} else {
|
||||
dynamicsList.addAll(res['data'].items);
|
||||
}
|
||||
offset = res['data'].offset;
|
||||
page++;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
}
|
4
lib/pages/dynamics/up_dynamic/index.dart
Normal file
4
lib/pages/dynamics/up_dynamic/index.dart
Normal file
@ -0,0 +1,4 @@
|
||||
library up_dynamics;
|
||||
|
||||
export './controller.dart';
|
||||
export './view.dart';
|
151
lib/pages/dynamics/up_dynamic/route_panel.dart
Normal file
151
lib/pages/dynamics/up_dynamic/route_panel.dart
Normal file
@ -0,0 +1,151 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/models/dynamics/up.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import '../controller.dart';
|
||||
import 'index.dart';
|
||||
|
||||
class OverlayPanel extends StatefulWidget {
|
||||
const OverlayPanel({super.key, required this.ctr, required this.upInfo});
|
||||
|
||||
final DynamicsController ctr;
|
||||
final UpItem upInfo;
|
||||
|
||||
@override
|
||||
State<OverlayPanel> createState() => _OverlayPanelState();
|
||||
}
|
||||
|
||||
class _OverlayPanelState extends State<OverlayPanel>
|
||||
with SingleTickerProviderStateMixin {
|
||||
static const itemPadding = EdgeInsets.symmetric(horizontal: 6, vertical: 0);
|
||||
final PageController pageController = PageController();
|
||||
late double contentWidth = 50;
|
||||
late List<UpItem> upList;
|
||||
late RxInt currentMid = (-1).obs;
|
||||
TabController? _tabController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
upList = widget.ctr.upData.value.upList!
|
||||
.map<UpItem>((element) => element)
|
||||
.toList();
|
||||
upList.removeAt(0);
|
||||
_tabController = TabController(length: upList.length, vsync: this);
|
||||
|
||||
currentMid.value = widget.upInfo.mid!;
|
||||
|
||||
pageController.addListener(() {
|
||||
int index = pageController.page!.round();
|
||||
int mid = upList[index].mid!;
|
||||
if (mid != currentMid.value) {
|
||||
currentMid.value = mid;
|
||||
_tabController?.animateTo(index,
|
||||
duration: Duration.zero, curve: Curves.linear);
|
||||
onClickUp(upList[index], index, type: 'pageChange');
|
||||
}
|
||||
});
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
int index =
|
||||
upList.indexWhere((element) => element.mid == widget.upInfo.mid);
|
||||
pageController.jumpToPage(index);
|
||||
onClickUp(widget.upInfo, index);
|
||||
_tabController?.animateTo(index,
|
||||
duration: Duration.zero, curve: Curves.linear);
|
||||
onClickUp(upList[index], index, type: 'pageChange');
|
||||
});
|
||||
}
|
||||
|
||||
void onClickUp(data, i, {type = 'click'}) {
|
||||
if (type == 'click') {
|
||||
pageController.jumpToPage(i);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: Get.width,
|
||||
height: Get.height,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
margin: EdgeInsets.fromLTRB(
|
||||
0,
|
||||
MediaQuery.of(context).padding.top + 4,
|
||||
0,
|
||||
MediaQuery.of(context).padding.bottom + 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 50,
|
||||
child: TabBar(
|
||||
controller: _tabController,
|
||||
dividerColor: Colors.transparent,
|
||||
automaticIndicatorColorAdjustment: false,
|
||||
tabAlignment: TabAlignment.start,
|
||||
padding: const EdgeInsets.only(left: 12, right: 12),
|
||||
indicatorPadding: EdgeInsets.zero,
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
indicator: const BoxDecoration(),
|
||||
labelPadding: itemPadding,
|
||||
indicatorWeight: 1,
|
||||
isScrollable: true,
|
||||
tabs: upList.map((e) => Tab(child: upItemBuild(e))).toList(),
|
||||
onTap: (index) {
|
||||
feedBack();
|
||||
EasyThrottle.throttle(
|
||||
'follow', const Duration(milliseconds: 200), () {
|
||||
onClickUp(upList[index], index);
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: PageView.builder(
|
||||
itemCount: upList.length,
|
||||
controller: pageController,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
margin: const EdgeInsets.fromLTRB(10, 12, 10, 0),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: UpDyanmicsPage(upInfo: upList[index], ctr: widget.ctr),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget upItemBuild(data) {
|
||||
return Obx(
|
||||
() => AnimatedOpacity(
|
||||
opacity: currentMid == data.mid ? 1 : 0.3,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeInOut,
|
||||
child: AnimatedScale(
|
||||
duration: const Duration(milliseconds: 200),
|
||||
scale: currentMid == data.mid ? 1 : 0.9,
|
||||
child: NetworkImgLayer(
|
||||
width: contentWidth,
|
||||
height: contentWidth,
|
||||
src: data.face,
|
||||
type: 'avatar',
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
178
lib/pages/dynamics/up_dynamic/view.dart
Normal file
178
lib/pages/dynamics/up_dynamic/view.dart
Normal file
@ -0,0 +1,178 @@
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/skeleton/dynamic_card.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/no_data.dart';
|
||||
import 'package:pilipala/models/dynamics/result.dart';
|
||||
import 'package:pilipala/models/dynamics/up.dart';
|
||||
import 'package:pilipala/pages/dynamics/up_dynamic/index.dart';
|
||||
|
||||
import '../index.dart';
|
||||
import '../widgets/dynamic_panel.dart';
|
||||
|
||||
class UpDyanmicsPage extends StatefulWidget {
|
||||
final UpItem upInfo;
|
||||
final DynamicsController ctr;
|
||||
|
||||
const UpDyanmicsPage({
|
||||
required this.upInfo,
|
||||
required this.ctr,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<UpDyanmicsPage> createState() => _UpDyanmicsPageState();
|
||||
}
|
||||
|
||||
class _UpDyanmicsPageState extends State<UpDyanmicsPage>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
late UpDynamicsController _upDynamicsController;
|
||||
final ScrollController scrollController = ScrollController();
|
||||
late Future _futureBuilderFuture;
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_upDynamicsController = Get.put(UpDynamicsController(widget.upInfo),
|
||||
tag: widget.upInfo.mid.toString());
|
||||
_futureBuilderFuture = _upDynamicsController.queryFollowDynamic();
|
||||
|
||||
scrollController.addListener(
|
||||
() async {
|
||||
if (scrollController.position.pixels >=
|
||||
scrollController.position.maxScrollExtent - 200) {
|
||||
EasyThrottle.throttle(
|
||||
'queryFollowDynamic', const Duration(seconds: 1), () {
|
||||
_upDynamicsController.queryFollowDynamic(type: 'onLoad');
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return CustomScrollView(
|
||||
controller: scrollController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
slivers: [
|
||||
SliverPersistentHeader(
|
||||
pinned: true,
|
||||
floating: true,
|
||||
delegate: _MySliverPersistentHeaderDelegate(
|
||||
child: Container(
|
||||
height: 50,
|
||||
padding: const EdgeInsets.fromLTRB(20, 4, 4, 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
width: 0.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
widget.upInfo.uname!,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.titleMedium!
|
||||
.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
icon: const Icon(Icons.close),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
FutureBuilder(
|
||||
future: _futureBuilderFuture,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
if (snapshot.data == null) {
|
||||
return const SliverToBoxAdapter(child: SizedBox());
|
||||
}
|
||||
Map? data = snapshot.data;
|
||||
if (data != null && data['status']) {
|
||||
List<DynamicItemModel> list =
|
||||
_upDynamicsController.dynamicsList;
|
||||
return Obx(
|
||||
() {
|
||||
if (list.isEmpty) {
|
||||
if (_upDynamicsController.isLoadingDynamic.value) {
|
||||
return skeleton();
|
||||
} else {
|
||||
return const NoData();
|
||||
}
|
||||
} else {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
return DynamicPanel(item: list[index]);
|
||||
},
|
||||
childCount: list.length,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return HttpError(
|
||||
errMsg: data?['msg'] ?? '请求异常',
|
||||
btnText: data?['code'] == -101 ? '去登录' : null,
|
||||
fn: () {},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// 骨架屏
|
||||
return skeleton();
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget skeleton() {
|
||||
return SliverList(
|
||||
delegate: SliverChildBuilderDelegate((context, index) {
|
||||
return const DynamicCardSkeleton();
|
||||
}, childCount: 5),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MySliverPersistentHeaderDelegate extends SliverPersistentHeaderDelegate {
|
||||
_MySliverPersistentHeaderDelegate({required this.child});
|
||||
final double _minExtent = 50;
|
||||
final double _maxExtent = 50;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
return child;
|
||||
}
|
||||
|
||||
@override
|
||||
double get maxExtent => _maxExtent;
|
||||
|
||||
@override
|
||||
double get minExtent => _minExtent;
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant _MySliverPersistentHeaderDelegate oldDelegate) {
|
||||
return true;
|
||||
}
|
||||
}
|
@ -3,13 +3,13 @@ import 'dart:async';
|
||||
import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart';
|
||||
import 'package:easy_debounce/easy_throttle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:hive/hive.dart';
|
||||
import 'package:pilipala/common/skeleton/dynamic_card.dart';
|
||||
import 'package:pilipala/common/widgets/http_error.dart';
|
||||
import 'package:pilipala/common/widgets/no_data.dart';
|
||||
import 'package:pilipala/models/dynamics/result.dart';
|
||||
import 'package:pilipala/plugin/pl_popup/index.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/main_stream.dart';
|
||||
import 'package:pilipala/utils/route_push.dart';
|
||||
@ -18,6 +18,7 @@ import 'package:pilipala/utils/storage.dart';
|
||||
import '../mine/controller.dart';
|
||||
import 'controller.dart';
|
||||
import 'widgets/dynamic_panel.dart';
|
||||
import 'up_dynamic/route_panel.dart';
|
||||
import 'widgets/up_panel.dart';
|
||||
|
||||
class DynamicsPage extends StatefulWidget {
|
||||
@ -202,7 +203,21 @@ class _DynamicsPageState extends State<DynamicsPage>
|
||||
}
|
||||
Map data = snapshot.data;
|
||||
if (data['status']) {
|
||||
return Obx(() => UpPanel(_dynamicsController.upData.value));
|
||||
return Obx(
|
||||
() => UpPanel(
|
||||
upData: _dynamicsController.upData.value,
|
||||
onClickUpCb: (data) {
|
||||
// _dynamicsController.onTapUp(data);
|
||||
Navigator.push(
|
||||
context,
|
||||
PlPopupRoute(
|
||||
child: OverlayPanel(
|
||||
ctr: _dynamicsController, upInfo: data),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return const SliverToBoxAdapter(
|
||||
child: SizedBox(height: 80),
|
||||
|
@ -4,13 +4,18 @@ import 'package:get/get.dart';
|
||||
import 'package:pilipala/common/widgets/network_img_layer.dart';
|
||||
import 'package:pilipala/models/dynamics/up.dart';
|
||||
import 'package:pilipala/models/live/item.dart';
|
||||
import 'package:pilipala/pages/dynamics/controller.dart';
|
||||
import 'package:pilipala/utils/feed_back.dart';
|
||||
import 'package:pilipala/utils/utils.dart';
|
||||
|
||||
class UpPanel extends StatefulWidget {
|
||||
final FollowUpModel upData;
|
||||
const UpPanel(this.upData, {Key? key}) : super(key: key);
|
||||
final Function? onClickUpCb;
|
||||
|
||||
const UpPanel({
|
||||
super.key,
|
||||
required this.upData,
|
||||
this.onClickUpCb,
|
||||
});
|
||||
|
||||
@override
|
||||
State<UpPanel> createState() => _UpPanelState();
|
||||
@ -33,27 +38,25 @@ class _UpPanelState extends State<UpPanel> {
|
||||
|
||||
void onClickUp(data, i) {
|
||||
currentMid = data.mid;
|
||||
Get.find<DynamicsController>().mid.value = data.mid;
|
||||
Get.find<DynamicsController>().upInfo.value = data;
|
||||
Get.find<DynamicsController>().onSelectUp(data.mid);
|
||||
int liveLen = liveList.length;
|
||||
int upLen = upList.length;
|
||||
double itemWidth = contentWidth + itemPadding.horizontal;
|
||||
double screenWidth = MediaQuery.sizeOf(context).width;
|
||||
double moveDistance = 0.0;
|
||||
if (itemWidth * (upList.length + liveList.length) <= screenWidth) {
|
||||
} else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
|
||||
moveDistance = (i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
|
||||
} else {
|
||||
moveDistance = (upLen + liveLen) * itemWidth + 46 - screenWidth;
|
||||
}
|
||||
data.hasUpdate = false;
|
||||
scrollController.animateTo(
|
||||
moveDistance,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.linear,
|
||||
);
|
||||
setState(() {});
|
||||
widget.onClickUpCb?.call(data);
|
||||
// int liveLen = liveList.length;
|
||||
// int upLen = upList.length;
|
||||
// double itemWidth = contentWidth + itemPadding.horizontal;
|
||||
// double screenWidth = MediaQuery.sizeOf(context).width;
|
||||
// double moveDistance = 0.0;
|
||||
// if (itemWidth * (upList.length + liveList.length) <= screenWidth) {
|
||||
// } else if ((upLen - i - 0.5) * itemWidth > screenWidth / 2) {
|
||||
// moveDistance = (i + liveLen + 0.5) * itemWidth + 46 - screenWidth / 2;
|
||||
// } else {
|
||||
// moveDistance = (upLen + liveLen) * itemWidth + 46 - screenWidth;
|
||||
// }
|
||||
// data.hasUpdate = false;
|
||||
// scrollController.animateTo(
|
||||
// moveDistance,
|
||||
// duration: const Duration(milliseconds: 200),
|
||||
// curve: Curves.linear,
|
||||
// );
|
||||
// setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
|
43
lib/plugin/pl_popup/index.dart
Normal file
43
lib/plugin/pl_popup/index.dart
Normal file
@ -0,0 +1,43 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PlPopupRoute extends PopupRoute<void> {
|
||||
PlPopupRoute({
|
||||
this.backgroudColor,
|
||||
this.alignment = Alignment.center,
|
||||
required this.child,
|
||||
this.onClick,
|
||||
});
|
||||
|
||||
/// backgroudColor
|
||||
final Color? backgroudColor;
|
||||
|
||||
/// child'alignment, default value: [Alignment.center]
|
||||
final Alignment alignment;
|
||||
|
||||
/// child
|
||||
final Widget child;
|
||||
|
||||
/// backgroudView action
|
||||
final Function? onClick;
|
||||
|
||||
@override
|
||||
Duration get transitionDuration => const Duration(milliseconds: 300);
|
||||
|
||||
@override
|
||||
bool get barrierDismissible => false;
|
||||
|
||||
@override
|
||||
Color get barrierColor => Colors.black54;
|
||||
|
||||
@override
|
||||
String? get barrierLabel => null;
|
||||
|
||||
@override
|
||||
Widget buildPage(
|
||||
BuildContext context,
|
||||
Animation<double> animation,
|
||||
Animation<double> secondaryAnimation,
|
||||
) {
|
||||
return child;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user