diff --git a/lib/pages/dynamics/controller.dart b/lib/pages/dynamics/controller.dart index 9bed3685..1cf9db9f 100644 --- a/lib/pages/dynamics/controller.dart +++ b/lib/pages/dynamics/controller.dart @@ -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 = []; queryFollowDynamic(); } + + // 点击up主 + void onTapUp(data) { + mid.value = data.mid; + upInfo.value = data; + onSelectUp(data.mid); + } } diff --git a/lib/pages/dynamics/up_dynamic/controller.dart b/lib/pages/dynamics/up_dynamic/controller.dart new file mode 100644 index 00000000..a30a0148 --- /dev/null +++ b/lib/pages/dynamics/up_dynamic/controller.dart @@ -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 dynamicsList = [].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; + } +} diff --git a/lib/pages/dynamics/up_dynamic/index.dart b/lib/pages/dynamics/up_dynamic/index.dart new file mode 100644 index 00000000..2de4c7e4 --- /dev/null +++ b/lib/pages/dynamics/up_dynamic/index.dart @@ -0,0 +1,4 @@ +library up_dynamics; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/dynamics/up_dynamic/route_panel.dart b/lib/pages/dynamics/up_dynamic/route_panel.dart new file mode 100644 index 00000000..40c725a8 --- /dev/null +++ b/lib/pages/dynamics/up_dynamic/route_panel.dart @@ -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 createState() => _OverlayPanelState(); +} + +class _OverlayPanelState extends State + with SingleTickerProviderStateMixin { + static const itemPadding = EdgeInsets.symmetric(horizontal: 6, vertical: 0); + final PageController pageController = PageController(); + late double contentWidth = 50; + late List upList; + late RxInt currentMid = (-1).obs; + TabController? _tabController; + + @override + void initState() { + super.initState(); + upList = widget.ctr.upData.value.upList! + .map((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', + ), + ), + ), + ); + } +} diff --git a/lib/pages/dynamics/up_dynamic/view.dart b/lib/pages/dynamics/up_dynamic/view.dart new file mode 100644 index 00000000..4b3fb0c7 --- /dev/null +++ b/lib/pages/dynamics/up_dynamic/view.dart @@ -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 createState() => _UpDyanmicsPageState(); +} + +class _UpDyanmicsPageState extends State + 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 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; + } +} diff --git a/lib/pages/dynamics/view.dart b/lib/pages/dynamics/view.dart index 0fc16dcf..38411613 100644 --- a/lib/pages/dynamics/view.dart +++ b/lib/pages/dynamics/view.dart @@ -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 } 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), diff --git a/lib/pages/dynamics/widgets/up_panel.dart b/lib/pages/dynamics/widgets/up_panel.dart index f8c973a0..3a0edcf6 100644 --- a/lib/pages/dynamics/widgets/up_panel.dart +++ b/lib/pages/dynamics/widgets/up_panel.dart @@ -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 createState() => _UpPanelState(); @@ -33,27 +38,25 @@ class _UpPanelState extends State { void onClickUp(data, i) { currentMid = data.mid; - Get.find().mid.value = data.mid; - Get.find().upInfo.value = data; - Get.find().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 diff --git a/lib/plugin/pl_popup/index.dart b/lib/plugin/pl_popup/index.dart new file mode 100644 index 00000000..678eb049 --- /dev/null +++ b/lib/plugin/pl_popup/index.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +class PlPopupRoute extends PopupRoute { + 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 animation, + Animation secondaryAnimation, + ) { + return child; + } +}