refactor dyn page

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-06-24 10:47:27 +08:00
parent 63fa031137
commit ebcbe8143b
10 changed files with 385 additions and 492 deletions

View File

@@ -33,22 +33,19 @@ import 'package:dio/dio.dart';
abstract final class DynamicsHttp { abstract final class DynamicsHttp {
@pragma('vm:notify-debugger-on-exception') @pragma('vm:notify-debugger-on-exception')
static Future<LoadingState<DynamicsDataModel>> followDynamic({ static Future<LoadingState<DynamicsDataModel>> followDynamic({
DynamicsTabType type = .all, int? hostMid,
String? offset, String? offset,
int? mid,
Set<int>? tempBannedList, Set<int>? tempBannedList,
DynamicsTabType type = .all,
}) async { }) async {
Map<String, dynamic> data = { final res = await Request.get(
if (type == .up) Api.followDynamic,
'host_mid': mid queryParameters: {
else ...{ if (type == .up) 'host_mid': hostMid else 'type': type.name,
'type': type.name, 'offset': ?offset,
'timezone_offset': '-480', 'features': Constants.dynFeatures,
}, },
'offset': offset, );
'features': Constants.dynFeatures,
};
final res = await Request.get(Api.followDynamic, queryParameters: data);
final code = res.data['code']; final code = res.data['code'];
if (code == 0) { if (code == 0) {
try { try {
@@ -61,7 +58,7 @@ abstract final class DynamicsHttp {
return await followDynamic( return await followDynamic(
type: type, type: type,
offset: data.offset, offset: data.offset,
mid: mid, hostMid: hostMid,
tempBannedList: tempBannedList, tempBannedList: tempBannedList,
); );
} }
@@ -89,17 +86,17 @@ abstract final class DynamicsHttp {
} }
} }
static Future<LoadingState<DynUpList>> dynUpList(String? offset) async { static Future<LoadingState<FollowUpModel>> dynUpList(String? offset) async {
final res = await Request.get( final res = await Request.get(
Api.dynUplist, Api.dynUplist,
queryParameters: { queryParameters: {
'offset': offset, 'offset': ?offset,
'platform': 'web', 'platform': 'web',
'web_location': 333.1365, 'web_location': 333.1365,
}, },
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return Success(DynUpList.fromJson(res.data['data'])); return Success(FollowUpModel.fromUpList(res.data['data']));
} else { } else {
return Error(res.data['message']); return Error(res.data['message']);
} }

View File

@@ -15,6 +15,7 @@ import 'package:brotli/brotli.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio/io.dart'; import 'package:dio/io.dart';
import 'package:dio_http2_adapter/dio_http2_adapter.dart'; import 'package:dio_http2_adapter/dio_http2_adapter.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
abstract final class Request { abstract final class Request {
static const _gzipDecoder = GZipDecoder(); static const _gzipDecoder = GZipDecoder();
@@ -163,15 +164,15 @@ abstract final class Request {
} }
// 日志拦截器 输出请求、响应内容 // 日志拦截器 输出请求、响应内容
// if (kDebugMode) { if (kDebugMode) {
// dio.interceptors.add( dio.interceptors.add(
// LogInterceptor( LogInterceptor(
// request: false, request: false,
// requestHeader: false, requestHeader: false,
// responseHeader: false, responseHeader: false,
// ), ),
// ); );
// } }
dio dio
..transformer = BackgroundTransformer() ..transformer = BackgroundTransformer()

View File

@@ -134,7 +134,7 @@ class Word {
style = json['style'] == null ? null : Style.fromJson(json['style']); style = json['style'] == null ? null : Style.fromJson(json['style']);
if (json['color'] case final String rawColor if (json['color'] case final String rawColor
when rawColor.startsWith('#')) { when rawColor.startsWith('#')) {
color = ColourUtils.parse2Int(json['color']); color = ColourUtils.parse2Int(rawColor);
} }
fontLevel = json['font_level']; fontLevel = json['font_level'];
} }

View File

@@ -1,41 +1,28 @@
import 'package:PiliPlus/utils/parse_int.dart'; import 'package:PiliPlus/utils/parse_int.dart';
class FollowUpModel { class FollowUpModel {
FollowUpModel({
this.liveUsers,
required this.upList,
});
LiveUsers? liveUsers; LiveUsers? liveUsers;
late List<UpItem> upList;
bool? hasMore;
String? offset;
FollowUpModel.fromJson(Map<String, dynamic> json) {
liveUsers = json['live_users'] != null
? LiveUsers.fromJson(json['live_users'])
: null;
upList =
(json['up_list']?['items'] as List?)
?.map<UpItem>((e) => UpItem.fromJson(e))
.toList() ??
<UpItem>[];
hasMore = json['up_list']?['has_more'];
offset = json['up_list']?['offset'];
}
}
class DynUpList {
List<UpItem>? upList; List<UpItem>? upList;
bool? hasMore; bool? hasMore;
String? offset; String? offset;
DynUpList.fromJson(Map<String, dynamic> json) { factory FollowUpModel.fromJson(Map<String, dynamic> json) {
upList = (json['items'] as List?) final model = FollowUpModel.fromUpList(json['up_list']);
?.map<UpItem>((e) => UpItem.fromJson(e)) final liveUsers = json['live_users'];
.toList(); if (liveUsers != null) {
hasMore = json['has_more']; model.liveUsers = LiveUsers.fromJson(liveUsers);
offset = json['offset']; }
return model;
}
FollowUpModel.fromUpList(Map<String, dynamic>? json) {
if (json != null) {
upList = (json['items'] as List?)
?.map((e) => UpItem.fromJson(e))
.toList();
hasMore = json['has_more'];
offset = json['offset'];
}
} }
} }

View File

@@ -1,151 +1,86 @@
import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/dynamic/dynamics_type.dart';
import 'package:PiliPlus/models/dynamics/up.dart'; import 'package:PiliPlus/models/dynamics/up.dart';
import 'package:PiliPlus/pages/common/common_controller.dart'; import 'package:PiliPlus/pages/common/common_data_controller.dart';
import 'package:PiliPlus/pages/dynamics_tab/controller.dart'; import 'package:PiliPlus/pages/dynamics_tab/controller.dart';
import 'package:PiliPlus/services/account_service.dart'; import 'package:PiliPlus/services/account_service.dart';
import 'package:PiliPlus/utils/extension/scroll_controller_ext.dart'; import 'package:PiliPlus/utils/extension/scroll_controller_ext.dart';
import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart';
import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/storage_pref.dart';
import 'package:flutter/material.dart' show ScrollController, TabController;
import 'package:get/get.dart'; import 'package:get/get.dart';
class DynamicsController extends GetxController class DynamicsController
with GetSingleTickerProviderStateMixin, ScrollOrRefreshMixin, AccountMixin { extends CommonDataController<FollowUpModel, FollowUpModel>
@override with AccountMixin {
final ScrollController scrollController = ScrollController(); String? offset;
late final TabController tabController;
late final RxInt mid = (-1).obs; bool isEnd = false;
late int currentMid = -1;
Set<int> tempBannedList = <int>{};
final Rx<LoadingState<FollowUpModel>> upState =
LoadingState<FollowUpModel>.loading().obs;
late bool _upEnd = false;
late bool showLiveUp = false; late bool showLiveUp = false;
final Set<int> tempBannedList = <int>{};
final upPanelPosition = Pref.upPanelPosition; final upPanelPosition = Pref.upPanelPosition;
late final innerController = Get.find<DynamicsTabController>();
@override @override
final AccountService accountService = Get.find<AccountService>(); final AccountService accountService = Get.find<AccountService>();
DynamicsTabController? get controller {
try {
return Get.find<DynamicsTabController>(
tag: DynamicsTabType.values[tabController.index].name,
);
} catch (_) {
return null;
}
}
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
tabController = TabController( queryData();
length: DynamicsTabType.values.length,
vsync: this,
);
queryFollowUp();
} }
void onLoadMoreUp() { @override
queryUpList(); bool customHandleResponse(bool isRefresh, Success<FollowUpModel> response) {
final res = response.response;
offset = res.offset;
if (!isRefresh) {
final lastData = loadingState.value.data;
if (res.upList case final upList?) {
lastData.upList!.addAll(upList);
}
res
..liveUsers = lastData.liveUsers
..upList = lastData.upList;
}
if (res.hasMore != true || offset.isNullOrEmpty) {
isEnd = true;
}
return false;
} }
Future<void> queryUpList() async { @override
if (isQuerying || _upEnd) return; Future<LoadingState<FollowUpModel>> customGetData() {
isQuerying = true; if (offset == null) {
return DynamicsHttp.followUp();
final res = await DynamicsHttp.dynUpList(upState.value.data.offset);
if (res case Success(:final response)) {
if (response.hasMore == false || response.offset.isNullOrEmpty) {
_upEnd = true;
}
final upData = upState.value.data
..hasMore = response.hasMore
..offset = response.offset;
final list = response.upList;
if (list != null && list.isNotEmpty) {
upData.upList.addAll(list);
upState.refresh();
}
} }
return DynamicsHttp.dynUpList(offset);
isQuerying = false;
} }
late bool isQuerying = false; Future<void> singleRefresh() {
Future<void> queryFollowUp() async { offset = null;
if (isQuerying) return; isEnd = false;
isQuerying = true; return super.onRefresh();
if (!accountService.isLogin.value) {
upState.value = const Error(null);
isQuerying = false;
return;
}
// reset
_upEnd = false;
final res = await DynamicsHttp.followUp();
if (res case final Success<FollowUpModel> i) {
final data = i.response;
if (data.hasMore == false || data.offset.isNullOrEmpty) {
_upEnd = true;
}
upState.value = Success(data);
} else {
upState.value = const Error(null);
}
isQuerying = false;
}
void onSelectUp(int mid) {
if (this.mid.value == mid) {
tabController.index = (mid == -1 ? 0 : 4);
if (mid == -1) {
queryFollowUp();
}
controller?.onReload();
return;
}
this.mid.value = mid;
tabController.index = (mid == -1 ? 0 : 4);
} }
@override @override
Future<void> onRefresh() { Future<void> onRefresh() {
_refreshFollowUp(); singleRefresh();
return controller!.onRefresh(); return innerController.onRefresh();
} }
void _refreshFollowUp() { @override
queryFollowUp(); Future<void> queryData([bool isRefresh = true]) {
if (!isRefresh && isEnd) return Future.value();
return super.queryData(isRefresh);
} }
@override @override
void animateToTop() { void animateToTop() {
controller?.animateToTop(); super.animateToTop();
scrollController.animToTop(); innerController.scrollController.animToTop();
} }
@override @override
void onClose() { void onChangeAccount(bool isLogin) => onReload();
mid.close();
tabController.dispose();
scrollController.dispose();
super.onClose();
}
@override
void onChangeAccount(bool isLogin) => _refreshFollowUp();
} }

View File

@@ -1,13 +1,11 @@
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/dynamic/dynamics_type.dart'; import 'package:PiliPlus/models/common/dynamic/dynamics_type.dart';
import 'package:PiliPlus/models/common/dynamic/up_panel_position.dart';
import 'package:PiliPlus/models/dynamics/up.dart'; import 'package:PiliPlus/models/dynamics/up.dart';
import 'package:PiliPlus/pages/dynamics/controller.dart'; import 'package:PiliPlus/pages/dynamics/controller.dart';
import 'package:PiliPlus/pages/dynamics/widgets/up_panel.dart'; import 'package:PiliPlus/pages/dynamics/widgets/up_panel.dart';
import 'package:PiliPlus/pages/dynamics_create/view.dart'; import 'package:PiliPlus/pages/dynamics_create/view.dart';
import 'package:PiliPlus/pages/dynamics_tab/controller.dart';
import 'package:PiliPlus/pages/dynamics_tab/view.dart'; import 'package:PiliPlus/pages/dynamics_tab/view.dart';
import 'package:PiliPlus/utils/extension/get_ext.dart';
import 'package:flutter/material.dart' hide DraggableScrollableSheet; import 'package:flutter/material.dart' hide DraggableScrollableSheet;
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -20,52 +18,59 @@ class DynamicsPage extends StatefulWidget {
class _DynamicsPageState extends State<DynamicsPage> class _DynamicsPageState extends State<DynamicsPage>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin {
final _dynamicsController = Get.putOrFind(DynamicsController.new); late ColorScheme colorScheme;
UpPanelPosition get upPanelPosition => _dynamicsController.upPanelPosition; late final DynamicsController _outerController;
late final DynamicsTabController _innerController;
@override @override
bool get wantKeepAlive => true; void initState() {
super.initState();
_outerController = Get.put(DynamicsController());
_innerController = Get.put(DynamicsTabController());
}
Widget _createDynamicBtn(ThemeData theme, {bool isRight = true}) => Center( @override
child: Container( void didChangeDependencies() {
width: 34, super.didChangeDependencies();
height: 34, colorScheme = ColorScheme.of(context);
margin: EdgeInsets.only(left: !isRight ? 16 : 0, right: isRight ? 16 : 0), }
child: IconButton(
tooltip: '发布动态', Widget _createDynamicBtn() => SizedBox(
style: ButtonStyle( width: 34,
padding: const WidgetStatePropertyAll(EdgeInsets.zero), height: 34,
backgroundColor: WidgetStatePropertyAll( child: IconButton(
theme.colorScheme.secondaryContainer, tooltip: '发布动态',
), style: ButtonStyle(
), padding: const WidgetStatePropertyAll(.zero),
onPressed: () => CreateDynPanel.onCreateDyn(context), backgroundColor: WidgetStatePropertyAll(
icon: Icon( colorScheme.secondaryContainer,
Icons.add,
size: 18,
color: theme.colorScheme.onSecondaryContainer,
), ),
), ),
onPressed: () => CreateDynPanel.onCreateDyn(context),
icon: Icon(
Icons.add,
size: 18,
color: colorScheme.onSecondaryContainer,
),
), ),
); );
Widget upPanelPart(ThemeData theme, {bool isTop = false}) { Widget upPanelPart({bool isTop = false}) {
final needBg = upPanelPosition.index > 2;
return Material( return Material(
type: needBg ? .canvas : .transparency, type: .transparency,
color: needBg ? theme.colorScheme.surface : null,
child: SizedBox( child: SizedBox(
width: isTop ? null : 64, width: isTop ? null : 64,
height: isTop ? 76 : null, height: isTop ? 76 : null,
child: NotificationListener<ScrollEndNotification>( child: NotificationListener<ScrollEndNotification>(
onNotification: (notification) { onNotification: (notification) {
final metrics = notification.metrics; final metrics = notification.metrics;
if (metrics.pixels >= metrics.maxScrollExtent - 300) { if (metrics.pixels >= metrics.maxScrollExtent - 300 &&
_dynamicsController.onLoadMoreUp(); !_outerController.isEnd) {
_outerController.onLoadMore();
} }
return false; return false;
}, },
child: Obx(() => _buildUpPanel(_dynamicsController.upState.value)), child: Obx(() => _buildUpPanel(_outerController.loadingState.value)),
), ),
), ),
); );
@@ -74,15 +79,14 @@ class _DynamicsPageState extends State<DynamicsPage>
Widget _buildUpPanel(LoadingState<FollowUpModel> upState) { Widget _buildUpPanel(LoadingState<FollowUpModel> upState) {
return switch (upState) { return switch (upState) {
Loading() => const SizedBox.shrink(), Loading() => const SizedBox.shrink(),
Success<FollowUpModel>() => UpPanel( Success<FollowUpModel>(:final response) => UpPanel(
dynamicsController: _dynamicsController, controller: _outerController,
upData: response,
), ),
Error() => Center( Error() => Center(
child: IconButton( child: IconButton(
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),
onPressed: () => _dynamicsController onPressed: _outerController.onReload,
..upState.value = LoadingState<FollowUpModel>.loading()
..queryFollowUp(),
), ),
), ),
}; };
@@ -91,79 +95,107 @@ class _DynamicsPageState extends State<DynamicsPage>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
final theme = Theme.of(context);
Widget? leading; final tab = _buildTab();
List<Widget>? actions; Widget child = DynamicsTabPage(controller: _innerController);
Widget child = tabBarView( switch (_outerController.upPanelPosition) {
controller: _dynamicsController.tabController,
children: DynamicsTabType.values
.map((e) => DynamicsTabPage(dynamicsType: e))
.toList(),
);
switch (upPanelPosition) {
case .top: case .top:
child = Column( return Column(
children: [ children: [
upPanelPart(theme, isTop: true), tab,
upPanelPart(isTop: true),
Expanded(child: child), Expanded(child: child),
], ],
); );
actions = [_createDynamicBtn(theme)];
case .leftFixed: case .leftFixed:
child = Row( child = Row(
children: [ children: [
upPanelPart(theme), upPanelPart(),
Expanded(child: child), Expanded(child: child),
], ],
); );
actions = [_createDynamicBtn(theme)];
case .rightFixed: case .rightFixed:
child = Row( child = Row(
children: [ children: [
Expanded(child: child), Expanded(child: child),
upPanelPart(theme), upPanelPart(),
], ],
); );
actions = [_createDynamicBtn(theme)];
} }
return Column( return Column(
children: [ children: [
AppBar( tab,
primary: false,
leading: leading,
leadingWidth: 50,
toolbarHeight: 50,
backgroundColor: Colors.transparent,
title: SizedBox(
height: 50,
child: TabBar(
dividerHeight: 0,
isScrollable: true,
tabAlignment: .center,
dividerColor: Colors.transparent,
labelColor: theme.colorScheme.primary,
indicatorColor: theme.colorScheme.primary,
controller: _dynamicsController.tabController,
unselectedLabelColor: theme.colorScheme.onSurface,
labelStyle: const TextStyle(fontSize: 13),
tabs: DynamicsTabType.values
.map((e) => Tab(text: e.label))
.toList(),
onTap: (index) {
if (!_dynamicsController.tabController.indexIsChanging) {
_dynamicsController.animateToTop();
}
},
),
),
actions: actions,
),
Expanded(child: child), Expanded(child: child),
], ],
); );
} }
Widget _buildTab() {
return Row(
children: [
Expanded(
child: Obx(
() {
final dynamicsType = _innerController.dynamicsType.value;
return Row(
children: DynamicsTabType.values.map((e) {
final isCurr = e == dynamicsType;
return InkWell(
onTap: e == .up && !isCurr
? null
: () {
if (isCurr) {
_outerController.animateToTop();
return;
}
if (dynamicsType == .up) {
_innerController.hostMid = -1;
_outerController.loadingState.refresh();
}
_innerController
..dynamicsType.value = e
..onReload();
},
child: DecoratedBox(
decoration: isCurr
? BoxDecoration(
border: Border(
bottom: BorderSide(
width: 2.0,
color: colorScheme.primary,
),
),
)
: const BoxDecoration(),
child: Container(
height: 46,
alignment: .center,
padding: const .symmetric(horizontal: 16),
child: Text(
e.label,
style: isCurr
? TextStyle(
fontSize: 13,
color: colorScheme.primary,
)
: const TextStyle(fontSize: 13),
),
),
),
);
}).toList(),
);
},
),
),
_createDynamicBtn(),
const SizedBox(width: 16),
],
);
}
@override
bool get wantKeepAlive => true;
} }

View File

@@ -10,65 +10,181 @@ import 'package:PiliPlus/utils/platform_utils.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class UpPanel extends StatefulWidget { class UpPanel extends StatelessWidget {
const UpPanel({ const UpPanel({
required this.dynamicsController,
super.key, super.key,
required this.upData,
required this.controller,
}); });
final DynamicsController dynamicsController; final FollowUpModel upData;
final DynamicsController controller;
@override
State<UpPanel> createState() => _UpPanelState();
}
class _UpPanelState extends State<UpPanel> {
late final controller = widget.dynamicsController;
late final isTop = controller.upPanelPosition == .top;
void toFollowPage() => Get.to(const LiveFollowPage());
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final accountService = controller.accountService;
if (!accountService.isLogin.value) {
return const SizedBox.shrink();
}
final theme = Theme.of(context); final theme = Theme.of(context);
final upData = controller.upState.value.data; final upList = upData.upList;
final List<UpItem> upList = upData.upList; final liveList = upData.liveUsers?.items;
final List<LiveUserItem>? liveList = upData.liveUsers?.items;
final isTop = controller.upPanelPosition == .top;
void toFollowPage() => Get.to(const LiveFollowPage());
Widget buildItem(UpItem item) {
final hostMid = controller.innerController.hostMid;
final isLive = item is LiveUserItem;
final isCurrent = isLive || hostMid == -1 || hostMid == item.mid;
final isAll = item.mid == -1;
void toMemberPage() => Get.toNamed('/member?mid=${item.mid}');
Widget avatar;
if (isAll) {
avatar = DecoratedBox(
decoration: const BoxDecoration(
shape: .circle,
color: Color(0xFF5CB67B),
),
child: Image.asset(
width: 38,
height: 38,
cacheWidth: 38.cacheSize,
Assets.logo2,
color: Colors.white,
),
);
} else {
avatar = Padding(
padding: const .symmetric(horizontal: 4),
child: NetworkImgLayer(
width: 38,
height: 38,
src: item.face,
type: .avatar,
),
);
if (isLive) {
avatar = Stack(
clipBehavior: .none,
children: [
avatar,
Positioned(
top: isLive && !isTop ? -5 : 0,
right: -6,
child: Badge(
label: const Text(' Live '),
textColor: theme.colorScheme.onSecondaryContainer,
backgroundColor: theme.colorScheme.secondaryContainer
.withValues(alpha: 0.75),
),
),
],
);
} else if (item.hasUpdate ?? false) {
avatar = Stack(
clipBehavior: .none,
children: [
avatar,
Positioned(
top: 0,
right: 4,
child: Badge(
smallSize: 8,
backgroundColor: theme.colorScheme.primary,
),
),
],
);
}
}
return SizedBox(
height: 76,
width: isTop ? 70 : null,
child: InkWell(
onTap: () {
if (isLive) {
PageUtils.toLiveRoom(item.roomId);
} else {
if (isAll) {
if (hostMid == -1) {
controller.singleRefresh();
}
controller.innerController.dynamicsType.value = .all;
} else {
item.hasUpdate = false;
controller.innerController.dynamicsType.value = .up;
}
controller.innerController
..hostMid = item.mid
..onReload();
(context as Element).markNeedsBuild();
}
},
// onDoubleTap: isLive ? () => _onSelect(data) : null,
onLongPress: !isAll ? toMemberPage : null,
onSecondaryTap: !isAll && !PlatformUtils.isMobile
? toMemberPage
: null,
child: Opacity(
opacity: isCurrent ? 1 : 0.6,
child: Column(
spacing: 4,
mainAxisSize: .min,
mainAxisAlignment: .center,
children: [
avatar,
Padding(
padding: const .symmetric(horizontal: 4),
child: Text(
isTop ? '${item.uname}\n' : item.uname!,
maxLines: 2,
textAlign: .center,
style: TextStyle(
color: hostMid == item.mid
? theme.colorScheme.primary
: theme.colorScheme.outline,
height: 1.1,
fontSize: 12.5,
),
),
),
],
),
),
),
);
}
return CustomScrollView( return CustomScrollView(
scrollDirection: isTop ? Axis.horizontal : Axis.vertical,
physics: const AlwaysScrollableScrollPhysics(),
controller: controller.scrollController, controller: controller.scrollController,
physics: const AlwaysScrollableScrollPhysics(),
scrollDirection: isTop ? .horizontal : .vertical,
slivers: [ slivers: [
SliverToBoxAdapter( SliverToBoxAdapter(
child: InkWell( child: InkWell(
onTap: () => setState(() { onTap: () {
controller.showLiveUp = !controller.showLiveUp; controller.showLiveUp = !controller.showLiveUp;
}), (context as Element).markNeedsBuild();
},
onLongPress: toFollowPage, onLongPress: toFollowPage,
onSecondaryTap: PlatformUtils.isMobile ? null : toFollowPage, onSecondaryTap: PlatformUtils.isMobile ? null : toFollowPage,
child: Container( child: Container(
alignment: Alignment.center, alignment: .center,
height: isTop ? 76 : 60, height: isTop ? 76 : 60,
padding: isTop ? const EdgeInsets.only(left: 12, right: 6) : null, padding: isTop ? const .only(left: 12, right: 6) : null,
child: Text.rich( child: Text.rich(
textAlign: TextAlign.center, textAlign: .center,
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: theme.colorScheme.primary, color: theme.colorScheme.primary,
), ),
TextSpan( TextSpan(
children: [ children: [
TextSpan( TextSpan(text: 'Live(${upData.liveUsers?.count ?? 0})'),
text: 'Live(${upData.liveUsers?.count ?? 0})',
),
if (!isTop) ...[ if (!isTop) ...[
const TextSpan(text: '\n'), const TextSpan(text: '\n'),
WidgetSpan( WidgetSpan(
alignment: PlaceholderAlignment.middle, alignment: .middle,
child: Icon( child: Icon(
controller.showLiveUp controller.showLiveUp
? Icons.expand_less ? Icons.expand_less
@@ -79,7 +195,7 @@ class _UpPanelState extends State<UpPanel> {
), ),
] else ] else
WidgetSpan( WidgetSpan(
alignment: PlaceholderAlignment.middle, alignment: .middle,
child: Icon( child: Icon(
controller.showLiveUp controller.showLiveUp
? Icons.keyboard_arrow_right ? Icons.keyboard_arrow_right
@@ -98,157 +214,32 @@ class _UpPanelState extends State<UpPanel> {
SliverList.builder( SliverList.builder(
itemCount: liveList.length, itemCount: liveList.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return upItemBuild(theme, liveList[index]); return buildItem(liveList[index]);
}, },
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: upItemBuild(theme, UpItem(face: '', uname: '全部动态', mid: -1)), child: buildItem(UpItem(face: '', uname: '全部动态', mid: -1)),
), ),
SliverToBoxAdapter( SliverToBoxAdapter(
child: Obx( child: Obx(
() => upItemBuild( () => buildItem(
theme,
UpItem( UpItem(
uname: '', uname: '',
face: accountService.face.value, face: controller.accountService.face.value,
mid: Accounts.main.mid, mid: Accounts.main.mid,
), ),
), ),
), ),
), ),
if (upList.isNotEmpty) if (upList != null && upList.isNotEmpty)
SliverList.builder( SliverList.builder(
itemCount: upList.length, itemCount: upList.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return upItemBuild(theme, upList[index]); return buildItem(upList[index]);
}, },
), ),
if (!isTop) const SliverToBoxAdapter(child: SizedBox(height: 200)), if (!isTop) const SliverToBoxAdapter(child: SizedBox(height: 200)),
], ],
); );
} }
void _onSelect(UpItem data) {
controller
..currentMid = data.mid
..onSelectUp(data.mid);
data.hasUpdate = false;
setState(() {});
}
Widget upItemBuild(ThemeData theme, UpItem data) {
final currentMid = controller.currentMid;
final isLive = data is LiveUserItem;
final isCurrent = isLive || currentMid == data.mid || currentMid == -1;
final isAll = data.mid == -1;
void toMemberPage() => Get.toNamed('/member?mid=${data.mid}');
Widget avatar;
if (isAll) {
avatar = DecoratedBox(
decoration: BoxDecoration(
shape: .circle,
border: Border.all(
width: 5,
color: const Color(0xFF5CB67B),
),
),
child: Image.asset(
width: 38,
height: 38,
cacheWidth: 38.cacheSize,
Assets.logo,
),
);
} else {
avatar = Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: NetworkImgLayer(
width: 38,
height: 38,
src: data.face,
type: .avatar,
),
);
if (isLive) {
avatar = Stack(
clipBehavior: .none,
children: [
avatar,
Positioned(
top: isLive && !isTop ? -5 : 0,
right: -6,
child: Badge(
label: const Text(' Live '),
textColor: theme.colorScheme.onSecondaryContainer,
backgroundColor: theme.colorScheme.secondaryContainer
.withValues(alpha: 0.75),
),
),
],
);
} else if (data.hasUpdate ?? false) {
avatar = Stack(
clipBehavior: .none,
children: [
avatar,
Positioned(
top: 0,
right: 4,
child: Badge(
smallSize: 8,
backgroundColor: theme.colorScheme.primary,
),
),
],
);
}
}
return SizedBox(
height: 76,
width: isTop ? 70 : null,
child: InkWell(
onTap: () {
if (isLive) {
PageUtils.toLiveRoom(data.roomId);
} else {
_onSelect(data);
}
},
// onDoubleTap: isLive ? () => _onSelect(data) : null,
onLongPress: !isAll ? toMemberPage : null,
onSecondaryTap: !isAll && !PlatformUtils.isMobile ? toMemberPage : null,
child: Opacity(
opacity: isCurrent ? 1 : 0.6,
child: Column(
spacing: 4,
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
avatar,
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text(
isTop ? '${data.uname}\n' : data.uname!,
maxLines: 2,
textAlign: TextAlign.center,
style: TextStyle(
color: currentMid == data.mid
? theme.colorScheme.primary
: theme.colorScheme.outline,
height: 1.1,
fontSize: 12.5,
),
),
),
],
),
),
),
);
}
} }

View File

@@ -1,5 +1,3 @@
import 'dart:async' show StreamSubscription;
import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/http/dynamics.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/http/msg.dart'; import 'package:PiliPlus/http/msg.dart';
@@ -17,53 +15,41 @@ import 'package:get/get.dart';
class DynamicsTabController class DynamicsTabController
extends CommonListController<DynamicsDataModel, DynamicItemModel> extends CommonListController<DynamicsDataModel, DynamicItemModel>
with AccountMixin, CommonReloadMixin { with AccountMixin, CommonReloadMixin {
DynamicsTabController({required this.dynamicsType}); int hostMid = -1;
final DynamicsTabType dynamicsType; String? offset;
String offset = ''; final Rx<DynamicsTabType> dynamicsType = Rx(.all);
int? mid;
late int flag = 0;
late final MainController mainController = Get.find<MainController>(); late final outerController = Get.find<DynamicsController>();
final dynamicsController = Get.find<DynamicsController>(); late final MainController _mainController = Get.find<MainController>();
StreamSubscription? _listener;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
queryData(); queryData();
if (dynamicsType == .up) {
_listener = dynamicsController.mid.listen((mid) {
if (mid != -1) {
flag++;
this.mid = mid;
onReload();
}
});
}
} }
@override @override
Future<void> onRefresh() { Future<void> onRefresh() {
if (dynamicsType == .all) { if (dynamicsType.value == .all) {
mainController.setDynCount(); _mainController.setDynCount();
} }
offset = ''; offset = null;
return super.onRefresh(); return super.onRefresh();
} }
@override @override
List<DynamicItemModel>? getDataList(DynamicsDataModel response) { List<DynamicItemModel>? getDataList(DynamicsDataModel response) {
offset = response.offset ?? ''; offset = response.offset;
return response.items; return response.items;
} }
@override @override
Future<LoadingState<DynamicsDataModel>> customGetData() => Future<LoadingState<DynamicsDataModel>> customGetData() =>
DynamicsHttp.followDynamic( DynamicsHttp.followDynamic(
type: dynamicsType,
offset: offset, offset: offset,
mid: mid, hostMid: hostMid,
tempBannedList: dynamicsController.tempBannedList, type: dynamicsType.value,
tempBannedList: outerController.tempBannedList,
); );
Future<void> onRemove(int index, dynamic dynamicId) async { Future<void> onRemove(int index, dynamic dynamicId) async {
@@ -79,7 +65,7 @@ class DynamicsTabController
} }
void onBlock(int index) { void onBlock(int index) {
if (dynamicsType != .up) { if (dynamicsType.value != .up) {
loadingState loadingState
..value.data!.removeAt(index) ..value.data!.removeAt(index)
..refresh(); ..refresh();
@@ -101,10 +87,4 @@ class DynamicsTabController
@override @override
void onChangeAccount(bool isLogin) => onReload(); void onChangeAccount(bool isLogin) => onReload();
@override
void onClose() {
_listener?.cancel();
super.onClose();
}
} }

View File

@@ -2,12 +2,9 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/dynamic/dynamics_type.dart';
import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/pages/dynamics/controller.dart';
import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart'; import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart';
import 'package:PiliPlus/pages/dynamics_tab/controller.dart'; import 'package:PiliPlus/pages/dynamics_tab/controller.dart';
import 'package:PiliPlus/utils/extension/get_ext.dart';
import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/global_data.dart';
import 'package:PiliPlus/utils/waterfall.dart'; import 'package:PiliPlus/utils/waterfall.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -15,53 +12,24 @@ import 'package:get/get.dart';
import 'package:waterfall_flow/waterfall_flow.dart' import 'package:waterfall_flow/waterfall_flow.dart'
hide SliverWaterfallFlowDelegateWithMaxCrossAxisExtent; hide SliverWaterfallFlowDelegateWithMaxCrossAxisExtent;
class DynamicsTabPage extends StatefulWidget { class DynamicsTabPage extends StatelessWidget with DynMixin {
const DynamicsTabPage({super.key, required this.dynamicsType}); DynamicsTabPage({
super.key,
required this.controller,
});
final DynamicsTabType dynamicsType; final DynamicsTabController controller;
@override
State<DynamicsTabPage> createState() => _DynamicsTabPageState();
}
class _DynamicsTabPageState extends State<DynamicsTabPage>
with AutomaticKeepAliveClientMixin, DynMixin {
DynamicsController dynamicsController = Get.putOrFind(DynamicsController.new);
late final DynamicsTabController controller;
@override
bool get wantKeepAlive => widget.dynamicsType == .all;
@override
void initState() {
super.initState();
controller = Get.putOrFind(
() =>
DynamicsTabController(dynamicsType: widget.dynamicsType)
..mid = dynamicsController.mid.value,
tag: widget.dynamicsType.name,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context);
return refreshIndicator( return refreshIndicator(
onRefresh: () { onRefresh: controller.outerController.onRefresh,
dynamicsController.queryFollowUp();
return controller.onRefresh();
},
child: CustomScrollView( child: CustomScrollView(
key: switch (widget.dynamicsType) {
.all => null,
.up => PageStorageKey('${widget.dynamicsType}${controller.flag}'),
_ => PageStorageKey(widget.dynamicsType),
},
physics: ReloadScrollPhysics(controller: controller), physics: ReloadScrollPhysics(controller: controller),
controller: controller.scrollController, controller: controller.scrollController,
slivers: [ slivers: [
SliverPadding( SliverPadding(
padding: const EdgeInsets.only(bottom: 100), padding: const .only(bottom: 100),
sliver: buildPage( sliver: buildPage(
Obx(() => _buildBody(controller.loadingState.value)), Obx(() => _buildBody(controller.loadingState.value)),
), ),

View File

@@ -311,13 +311,15 @@ abstract final class RequestUtils {
await Future.delayed(const Duration(milliseconds: 450)); await Future.delayed(const Duration(milliseconds: 450));
final res = await DynamicsHttp.dynamicDetail(id: id); final res = await DynamicsHttp.dynamicDetail(id: id);
if (res case final Success<DynamicItemModel> e) { if (res case final Success<DynamicItemModel> e) {
final ctr = Get.find<DynamicsTabController>(tag: 'all'); final ctr = Get.find<DynamicsTabController>();
if (ctr.loadingState.value case Success(:final response?)) { if (ctr.dynamicsType.value == .all) {
response.insert(0, e.response); if (ctr.loadingState.value case Success(:final response?)) {
ctr.loadingState.refresh(); response.insert(0, e.response);
return; ctr.loadingState.refresh();
return;
}
ctr.loadingState.value = Success([e.response]);
} }
ctr.loadingState.value = Success([e.response]);
} }
} catch (e) { } catch (e) {
if (kDebugMode) debugPrint('create dyn $e'); if (kDebugMode) debugPrint('create dyn $e');