diff --git a/lib/http/msg.dart b/lib/http/msg.dart index 818da833..7e410646 100644 --- a/lib/http/msg.dart +++ b/lib/http/msg.dart @@ -1,5 +1,10 @@ import 'dart:math'; import 'package:PiliPlus/http/constants.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/msg/msgfeed_at_me.dart'; +import 'package:PiliPlus/models/msg/msgfeed_like_me.dart'; +import 'package:PiliPlus/models/msg/msgfeed_reply_me.dart'; +import 'package:PiliPlus/models/msg/msgfeed_sys_msg.dart'; import 'package:PiliPlus/pages/dynamics/view.dart' show ReplyOption; import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; @@ -12,7 +17,8 @@ import 'api.dart'; import 'init.dart'; class MsgHttp { - static Future msgFeedReplyMe({int cursor = -1, int cursorTime = -1}) async { + static Future msgFeedReplyMe( + {int cursor = -1, int cursorTime = -1}) async { var res = await Request().get(Api.msgFeedReply, queryParameters: { 'id': cursor == -1 ? null : cursor, 'reply_time': cursorTime == -1 ? null : cursorTime, @@ -21,20 +27,15 @@ class MsgHttp { 'build': '8350200', }); if (res.data['code'] == 0) { - return { - 'status': true, - 'data': res.data['data'], - }; + MsgFeedReplyMe data = MsgFeedReplyMe.fromJson(res.data['data']); + return LoadingState.success(data); } else { - return { - 'status': false, - 'date': [], - 'msg': res.data['message'], - }; + return LoadingState.error(res.data['message']); } } - static Future msgFeedAtMe({int cursor = -1, int cursorTime = -1}) async { + static Future msgFeedAtMe( + {int cursor = -1, int cursorTime = -1}) async { var res = await Request().get(Api.msgFeedAt, queryParameters: { 'id': cursor == -1 ? null : cursor, 'at_time': cursorTime == -1 ? null : cursorTime, @@ -43,20 +44,15 @@ class MsgHttp { 'build': '8350200', }); if (res.data['code'] == 0) { - return { - 'status': true, - 'data': res.data['data'], - }; + MsgFeedAtMe data = MsgFeedAtMe.fromJson(res.data['data']); + return LoadingState.success(data); } else { - return { - 'status': false, - 'date': [], - 'msg': res.data['message'], - }; + return LoadingState.error(res.data['message']); } } - static Future msgFeedLikeMe({int cursor = -1, int cursorTime = -1}) async { + static Future msgFeedLikeMe( + {int cursor = -1, int cursorTime = -1}) async { var res = await Request().get(Api.msgFeedLike, queryParameters: { 'id': cursor == -1 ? null : cursor, 'like_time': cursorTime == -1 ? null : cursorTime, @@ -65,35 +61,26 @@ class MsgHttp { 'build': '8350200', }); if (res.data['code'] == 0) { - return { - 'status': true, - 'data': res.data['data'], - }; + MsgFeedLikeMe data = MsgFeedLikeMe.fromJson(res.data['data']); + return LoadingState.success(data); } else { - return { - 'status': false, - 'date': [], - 'msg': res.data['message'], - }; + return LoadingState.error(res.data['message']); } } - static Future msgFeedNotify({int cursor = -1, int pageSize = 20}) async { + static Future msgFeedNotify( + {int cursor = -1, int pageSize = 20}) async { var res = await Request().get(Api.msgSysNotify, queryParameters: { 'cursor': cursor == -1 ? null : cursor, 'page_size': pageSize, }); if (res.data['code'] == 0) { - return { - 'status': true, - 'data': res.data['data'], - }; + List? list = (res.data['data'] as List?) + ?.map((e) => SystemNotifyList.fromJson(e)) + .toList(); + return LoadingState.success(list); } else { - return { - 'status': false, - 'date': [], - 'msg': res.data['message'], - }; + return LoadingState.error(res.data['message']); } } diff --git a/lib/models/msg/msgfeed_like_me.dart b/lib/models/msg/msgfeed_like_me.dart index a3e5d6b1..2a6562ad 100644 --- a/lib/models/msg/msgfeed_like_me.dart +++ b/lib/models/msg/msgfeed_like_me.dart @@ -5,8 +5,7 @@ class MsgFeedLikeMe { MsgFeedLikeMe({latest, total}); MsgFeedLikeMe.fromJson(Map json) { - latest = - json['latest'] != null ? Latest.fromJson(json['latest']) : null; + latest = json['latest'] != null ? Latest.fromJson(json['latest']) : null; total = json['total'] != null ? Total.fromJson(json['total']) : null; } @@ -50,13 +49,7 @@ class LikeMeItems { int? likeTime; int? noticeState; - LikeMeItems( - {id, - users, - item, - counts, - likeTime, - noticeState}); + LikeMeItems({id, users, item, counts, likeTime, noticeState}); LikeMeItems.fromJson(Map json) { id = json['id']; @@ -92,13 +85,7 @@ class Users { String? midLink; bool? follow; - Users( - {mid, - fans, - nickname, - avatar, - midLink, - follow}); + Users({mid, fans, nickname, avatar, midLink, follow}); Users.fromJson(Map json) { mid = json['mid']; @@ -139,19 +126,19 @@ class Item { Item( {itemId, - pid, - type, - business, - businessId, - replyBusinessId, - likeBusinessId, - title, - desc, - image, - uri, - detailName, - nativeUri, - ctime}); + pid, + type, + business, + businessId, + replyBusinessId, + likeBusinessId, + title, + desc, + image, + uri, + detailName, + nativeUri, + ctime}); Item.fromJson(Map json) { itemId = json['item_id']; @@ -197,8 +184,7 @@ class Total { Total({cursor, items}); Total.fromJson(Map json) { - cursor = - json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null; + cursor = json['cursor'] != null ? Cursor.fromJson(json['cursor']) : null; if (json['items'] != null) { items = []; json['items'].forEach((v) { @@ -235,4 +221,4 @@ class Cursor { data['time'] = time; return data; } -} \ No newline at end of file +} diff --git a/lib/pages/msg_feed_top/at_me/controller.dart b/lib/pages/msg_feed_top/at_me/controller.dart index 2f21aa48..3804dbee 100644 --- a/lib/pages/msg_feed_top/at_me/controller.dart +++ b/lib/pages/msg_feed_top/at_me/controller.dart @@ -1,43 +1,43 @@ -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:get/get.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/pages/common/common_controller.dart'; +import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/http/msg.dart'; import 'package:PiliPlus/models/msg/msgfeed_at_me.dart'; -class AtMeController extends GetxController { - RxList msgFeedAtMeList = [].obs; - bool isLoading = false; +class AtMeController extends CommonController { int cursor = -1; int cursorTime = -1; - bool isEnd = false; - Future queryMsgFeedAtMe() async { - if (isLoading) return; - isLoading = true; - var res = await MsgHttp.msgFeedAtMe(cursor: cursor, cursorTime: cursorTime); - isLoading = false; - if (res['status']) { - MsgFeedAtMe data = MsgFeedAtMe.fromJson(res['data']); - isEnd = data.cursor?.isEnd ?? false; - if (cursor == -1) { - msgFeedAtMeList.assignAll(data.items!); - } else { - msgFeedAtMeList.addAll(data.items!); - } - cursor = data.cursor?.id ?? -1; - cursorTime = data.cursor?.time ?? -1; - } else { - SmartDialog.showToast(res['msg']); + @override + void onInit() { + super.onInit(); + queryData(); + } + + @override + bool customHandleResponse(Success response) { + MsgFeedAtMe data = response.response; + if (data.cursor?.isEnd == true || data.items.isNullOrEmpty) { + isEnd = true; } + cursor = data.cursor?.id ?? -1; + cursorTime = data.cursor?.time ?? -1; + if (currentPage != 1 && loadingState.value is Success) { + data.items ??= []; + data.items!.insert(0, (loadingState.value as Success).response); + } + loadingState.value = LoadingState.success(data.items); + return true; } - Future onLoad() async { - if (isEnd) return; - queryMsgFeedAtMe(); - } - - Future onRefresh() async { + @override + Future onRefresh() { cursor = -1; cursorTime = -1; - queryMsgFeedAtMe(); + return super.onRefresh(); } + + @override + Future customGetData() => + MsgHttp.msgFeedAtMe(cursor: cursor, cursorTime: cursorTime); } diff --git a/lib/pages/msg_feed_top/at_me/view.dart b/lib/pages/msg_feed_top/at_me/view.dart index 585d9fd7..b8d58425 100644 --- a/lib/pages/msg_feed_top/at_me/view.dart +++ b/lib/pages/msg_feed_top/at_me/view.dart @@ -1,11 +1,11 @@ import 'package:PiliPlus/common/widgets/loading_widget.dart'; +import 'package:PiliPlus/common/widgets/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; -import 'package:easy_debounce/easy_throttle.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:PiliPlus/common/widgets/network_img_layer.dart'; -import '../../../utils/app_scheme.dart'; import 'controller.dart'; class AtMePage extends StatefulWidget { @@ -17,31 +17,6 @@ class AtMePage extends StatefulWidget { class _AtMePageState extends State { late final AtMeController _atMeController = Get.put(AtMeController()); - final ScrollController _scrollController = ScrollController(); - - @override - void initState() { - _atMeController.queryMsgFeedAtMe(); - super.initState(); - _scrollController.addListener(_scrollListener); - } - - @override - void dispose() { - _scrollController.removeListener(_scrollListener); - _scrollController.dispose(); - super.dispose(); - } - - Future _scrollListener() async { - if (_scrollController.position.pixels >= - _scrollController.position.maxScrollExtent - 200) { - EasyThrottle.throttle('my-throttler', const Duration(milliseconds: 800), - () async { - await _atMeController.onLoad(); - }); - } - } @override Widget build(BuildContext context) { @@ -53,53 +28,51 @@ class _AtMePageState extends State { onRefresh: () async { await _atMeController.onRefresh(); }, - child: Obx( - () { - // TODO: refactor - if (_atMeController.msgFeedAtMeList.isEmpty) { - if (_atMeController.cursor == -1 && - _atMeController.cursorTime == -1) { - return const Center( - child: CircularProgressIndicator(), - ); - } else { - return scrollErrorWidget( - callback: _atMeController.queryMsgFeedAtMe); - } - } - return ListView.separated( - controller: _scrollController, - itemCount: _atMeController.msgFeedAtMeList.length, + child: Obx(() => _buildBody(_atMeController.loadingState.value)), + ), + ); + } + + Widget _buildBody(LoadingState loadingState) { + return switch (loadingState) { + Loading() => loadingWidget, + Success() => (loadingState.response as List?)?.isNotEmpty == true + ? ListView.separated( + itemCount: loadingState.response.length, physics: const AlwaysScrollableScrollPhysics(), - itemBuilder: (context, int i) { + padding: EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom + 80), + itemBuilder: (context, int index) { + if (index == loadingState.response.length - 1) { + _atMeController.onLoadMore(); + } return ListTile( onTap: () { String? nativeUri = - _atMeController.msgFeedAtMeList[i].item?.nativeUri; + loadingState.response[index].item?.nativeUri; if (nativeUri != null) { PiliScheme.routePushFromUrl(nativeUri); } - // SmartDialog.showToast("跳转至:$nativeUri(暂未实现)"); }, leading: NetworkImgLayer( width: 45, height: 45, type: 'avatar', - src: _atMeController.msgFeedAtMeList[i].user?.avatar, + src: loadingState.response[index].user?.avatar, ), title: Text( - "${_atMeController.msgFeedAtMeList[i].user?.nickname} " - "在${_atMeController.msgFeedAtMeList[i].item?.business}中@了我", - style: Theme.of(context).textTheme.titleMedium!.copyWith( - color: Theme.of(context).colorScheme.primary, - )), + "${loadingState.response[index].user?.nickname} " + "在${loadingState.response[index].item?.business}中@了我", + style: Theme.of(context).textTheme.titleMedium!.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 4), Text( - _atMeController - .msgFeedAtMeList[i].item?.sourceContent ?? + loadingState.response[index].item?.sourceContent ?? "", maxLines: 3, overflow: TextOverflow.ellipsis, @@ -110,14 +83,13 @@ class _AtMePageState extends State { color: Theme.of(context).colorScheme.outline)) ], ), - trailing: _atMeController.msgFeedAtMeList[i].item?.image != - null && - _atMeController.msgFeedAtMeList[i].item?.image != "" + trailing: loadingState.response[index].item?.image != null && + loadingState.response[index].item?.image != "" ? NetworkImgLayer( width: 45, height: 45, type: 'cover', - src: _atMeController.msgFeedAtMeList[i].item?.image, + src: loadingState.response[index].item?.image, ) : null, ); @@ -130,10 +102,13 @@ class _AtMePageState extends State { color: Colors.grey.withOpacity(0.1), ); }, - ); - }, + ) + : scrollErrorWidget(callback: _atMeController.onReload), + Error() => scrollErrorWidget( + errMsg: loadingState.errMsg, + callback: _atMeController.onReload, ), - ), - ); + LoadingState() => throw UnimplementedError(), + }; } } diff --git a/lib/pages/msg_feed_top/like_me/controller.dart b/lib/pages/msg_feed_top/like_me/controller.dart index a96dc3bd..d6ea1e48 100644 --- a/lib/pages/msg_feed_top/like_me/controller.dart +++ b/lib/pages/msg_feed_top/like_me/controller.dart @@ -1,47 +1,57 @@ -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:get/get.dart'; +import 'package:PiliPlus/common/widgets/pair.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/pages/common/common_controller.dart'; import 'package:PiliPlus/http/msg.dart'; +import 'package:PiliPlus/utils/extension.dart'; import '../../../models/msg/msgfeed_like_me.dart'; -class LikeMeController extends GetxController { - RxList msgFeedLikeMeLatestList = [].obs; - RxList msgFeedLikeMeTotalList = [].obs; - bool isLoading = false; +class LikeMeController extends CommonController { int cursor = -1; int cursorTime = -1; - bool isEnd = false; - Future queryMsgFeedLikeMe() async { - if (isLoading) return; - isLoading = true; - var res = - await MsgHttp.msgFeedLikeMe(cursor: cursor, cursorTime: cursorTime); - isLoading = false; - if (res['status']) { - MsgFeedLikeMe data = MsgFeedLikeMe.fromJson(res['data']); - isEnd = data.total?.cursor?.isEnd ?? false; - if (cursor == -1) { - msgFeedLikeMeLatestList.assignAll(data.latest?.items ?? []); - msgFeedLikeMeTotalList.assignAll(data.total?.items ?? []); - } else { - msgFeedLikeMeLatestList.addAll(data.latest?.items ?? []); - msgFeedLikeMeTotalList.addAll(data.total?.items ?? []); - } - cursor = data.total?.cursor?.id ?? -1; - cursorTime = data.total?.cursor?.time ?? -1; - } else { - SmartDialog.showToast(res['msg']); + @override + void onInit() { + super.onInit(); + queryData(); + } + + @override + bool customHandleResponse(Success response) { + MsgFeedLikeMe data = response.response; + if (data.total?.cursor?.isEnd == true || + data.total?.items.isNullOrEmpty == true) { + isEnd = true; } + cursor = data.total?.cursor?.id ?? -1; + cursorTime = data.total?.cursor?.time ?? -1; + List latest = []; + List total = []; + if (data.latest?.items?.isNotEmpty == true) { + latest.addAll(data.latest!.items!); + } + if (data.total?.items?.isNotEmpty == true) { + total.addAll(data.total!.items!); + } + if (currentPage != 1 && loadingState.value is Success) { + Pair, List> pair = + (loadingState.value as Success).response; + latest.insertAll(0, pair.first); + total.insertAll(0, pair.second); + } + loadingState.value = LoadingState.success( + Pair(first: latest, second: total), + ); + return true; } - Future onLoad() async { - if (isEnd) return; - queryMsgFeedLikeMe(); - } - - Future onRefresh() async { + @override + Future onRefresh() { cursor = -1; cursorTime = -1; - queryMsgFeedLikeMe(); + return super.onRefresh(); } + + @override + Future customGetData() => + MsgHttp.msgFeedLikeMe(cursor: cursor, cursorTime: cursorTime); } diff --git a/lib/pages/msg_feed_top/like_me/view.dart b/lib/pages/msg_feed_top/like_me/view.dart index 8b535abb..59aeeba7 100644 --- a/lib/pages/msg_feed_top/like_me/view.dart +++ b/lib/pages/msg_feed_top/like_me/view.dart @@ -1,10 +1,12 @@ +import 'package:PiliPlus/common/widgets/loading_widget.dart'; +import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; -import 'package:easy_debounce/easy_throttle.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/msg/msgfeed_like_me.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:PiliPlus/common/widgets/network_img_layer.dart'; -import '../../../models/msg/msgfeed_like_me.dart'; import '../../../utils/app_scheme.dart'; import 'controller.dart'; @@ -17,31 +19,6 @@ class LikeMePage extends StatefulWidget { class _LikeMePageState extends State { late final LikeMeController _likeMeController = Get.put(LikeMeController()); - final ScrollController _scrollController = ScrollController(); - - @override - void initState() { - _likeMeController.queryMsgFeedLikeMe(); - super.initState(); - _scrollController.addListener(_scrollListener); - } - - @override - void dispose() { - _scrollController.removeListener(_scrollListener); - _scrollController.dispose(); - super.dispose(); - } - - Future _scrollListener() async { - if (_scrollController.position.pixels >= - _scrollController.position.maxScrollExtent - 200) { - EasyThrottle.throttle('my-throttler', const Duration(milliseconds: 800), - () async { - await _likeMeController.onLoad(); - }); - } - } @override Widget build(BuildContext context) { @@ -53,155 +30,173 @@ class _LikeMePageState extends State { onRefresh: () async { await _likeMeController.onRefresh(); }, - // TODO: refactor - child: SingleChildScrollView( - controller: _scrollController, - child: LayoutBuilder( - builder: (BuildContext context, BoxConstraints constraints) { - return Obx( - () { - if (_likeMeController.msgFeedLikeMeLatestList.isEmpty && - _likeMeController.msgFeedLikeMeTotalList.isEmpty) { - return const Center( - child: CircularProgressIndicator(), - ); - } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (_likeMeController - .msgFeedLikeMeLatestList.isNotEmpty) ...[ - Text(" 最新", - style: Theme.of(context) - .textTheme - .labelMedium! - .copyWith( - color: - Theme.of(context).colorScheme.outline)), - LikeMeList( - msgFeedLikeMeList: - _likeMeController.msgFeedLikeMeLatestList), - ], - if (_likeMeController - .msgFeedLikeMeTotalList.isNotEmpty) ...[ - Text(" 累计", - style: Theme.of(context) - .textTheme - .labelMedium! - .copyWith( - color: - Theme.of(context).colorScheme.outline)), - LikeMeList( - msgFeedLikeMeList: - _likeMeController.msgFeedLikeMeTotalList), - ] - ]); - }, - ); - }), - ), + child: Obx(() => _buildBody(_likeMeController.loadingState.value)), ), ); } -} -class LikeMeList extends StatelessWidget { - const LikeMeList({ - super.key, - required this.msgFeedLikeMeList, - }); - final RxList msgFeedLikeMeList; + Widget _buildBody(LoadingState loadingState) { + return switch (loadingState) { + Loading() => loadingWidget, + Success() => () { + Pair, List> pair = + loadingState.response; - @override - Widget build(BuildContext context) { - return ListView.separated( - itemCount: msgFeedLikeMeList.length, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, int i) { - return ListTile( - onTap: () { - String? nativeUri = msgFeedLikeMeList[i].item?.nativeUri; - if (nativeUri != null) { - PiliScheme.routePushFromUrl(nativeUri); + int length = pair.first.length + pair.second.length; + if (pair.first.isNotEmpty) { + length++; + } + if (pair.second.isNotEmpty) { + length++; + } + + LikeMeItems getCurrentItem(int index) { + if (pair.first.isEmpty) { + return pair.second[index - 1]; + } else { + return index <= pair.first.length + ? pair.first[index - 1] + : pair.second[index - pair.first.length - 2]; } - // SmartDialog.showToast("跳转至:$nativeUri(暂未实现)"); - }, - leading: Column( - children: [ - const Spacer(), - SizedBox( - width: 50, - height: 50, - child: Stack( - children: [ - for (var j = 0; - j < msgFeedLikeMeList[i].users!.length && j < 4; - j++) ...[ - Positioned( - left: 15 * (j % 2).toDouble(), - top: 15 * (j ~/ 2).toDouble(), - child: NetworkImgLayer( - width: msgFeedLikeMeList[i].users!.length > 1 - ? 30 - : 45, - height: msgFeedLikeMeList[i].users!.length > 1 - ? 30 - : 45, - type: 'avatar', - src: msgFeedLikeMeList[i].users![j].avatar, - )), - ] - ], - )), - const Spacer(), - ], - ), - title: Text( - // "${msgFeedLikeMeList[i].users!.map((e) => e.nickname).join("/")}" - "${msgFeedLikeMeList[i].users?[0].nickname}" - "${msgFeedLikeMeList[i].users!.length > 1 ? '、' + msgFeedLikeMeList[i].users![1].nickname.toString() + ' 等' : ''} " - "${msgFeedLikeMeList[i].counts! > 1 ? '共 ' + msgFeedLikeMeList[i].counts.toString() + ' 人' : ''}" - "赞了我的${msgFeedLikeMeList[i].item?.business}", - style: Theme.of(context).textTheme.titleSmall!.copyWith( - height: 1.5, color: Theme.of(context).colorScheme.primary), - maxLines: 2, - overflow: TextOverflow.ellipsis, - ), - subtitle: msgFeedLikeMeList[i].item?.title != null && - msgFeedLikeMeList[i].item?.title != "" - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 4), - Text(msgFeedLikeMeList[i].item?.title ?? "", - maxLines: 3, + } + + return length > 0 + ? ListView.separated( + itemCount: length, + padding: EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom + 80), + physics: const AlwaysScrollableScrollPhysics(), + itemBuilder: (context, int index) { + if (index == length - 1) { + _likeMeController.onLoadMore(); + } + + // title + if (index == 0) { + return Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + pair.first.isNotEmpty ? '最新' : '累计', + style: Theme.of(context) + .textTheme + .labelLarge! + .copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ); + } else if (pair.first.isNotEmpty && + index == pair.first.length + 1) { + return Padding( + padding: const EdgeInsets.only(left: 16), + child: Text( + "累计", + style: Theme.of(context) + .textTheme + .labelLarge! + .copyWith( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ); + } + + // item + final item = getCurrentItem(index); + return ListTile( + onTap: () { + String? nativeUri = item.item?.nativeUri; + if (nativeUri != null) { + PiliScheme.routePushFromUrl(nativeUri); + } + }, + leading: Column( + children: [ + const Spacer(), + SizedBox( + width: 50, + height: 50, + child: Stack( + children: [ + for (var j = 0; + j < item.users!.length && j < 4; + j++) ...[ + Positioned( + left: 15 * (j % 2).toDouble(), + top: 15 * (j ~/ 2).toDouble(), + child: NetworkImgLayer( + width: + item.users!.length > 1 ? 30 : 45, + height: + item.users!.length > 1 ? 30 : 45, + type: 'avatar', + src: item.users![j].avatar, + )), + ] + ], + )), + const Spacer(), + ], + ), + title: Text( + // "${msgFeedLikeMeList[i].users!.map((e) => e.nickname).join("/")}" + "${item.users?[0].nickname}" + "${item.users!.length > 1 ? '、${item.users![1].nickname} 等' : ''} " + "${item.counts! > 1 ? '共 ${item.counts} 人' : ''}" + "赞了我的${item.item?.business}", + style: Theme.of(context).textTheme.titleSmall!.copyWith( + height: 1.5, + color: Theme.of(context).colorScheme.primary), + maxLines: 2, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: Theme.of(context).colorScheme.outline, - height: 1.5)) - ], + ), + subtitle: + item.item?.title != null && item.item?.title != "" + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Text(item.item?.title ?? "", + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: Theme.of(context) + .colorScheme + .outline, + height: 1.5)) + ], + ) + : null, + trailing: + item.item?.image != null && item.item?.image != "" + ? NetworkImgLayer( + width: 45, + height: 45, + type: 'cover', + src: item.item?.image, + ) + : null, + ); + }, + separatorBuilder: (BuildContext context, int index) { + return Divider( + indent: 72, + endIndent: 20, + height: 6, + color: Colors.grey.withOpacity(0.1), + ); + }, ) - : null, - trailing: msgFeedLikeMeList[i].item?.image != null && - msgFeedLikeMeList[i].item?.image != "" - ? NetworkImgLayer( - width: 45, - height: 45, - type: 'cover', - src: msgFeedLikeMeList[i].item?.image, - ) - : null, - ); - }, - separatorBuilder: (BuildContext context, int index) { - return Divider( - indent: 72, - endIndent: 20, - height: 6, - color: Colors.grey.withOpacity(0.1), - ); - }, - ); + : scrollErrorWidget(callback: _likeMeController.onReload); + }(), + Error() => scrollErrorWidget( + errMsg: loadingState.errMsg, + callback: _likeMeController.onReload, + ), + LoadingState() => throw UnimplementedError(), + }; } } diff --git a/lib/pages/msg_feed_top/reply_me/controller.dart b/lib/pages/msg_feed_top/reply_me/controller.dart index 3f8d2081..63f403cd 100644 --- a/lib/pages/msg_feed_top/reply_me/controller.dart +++ b/lib/pages/msg_feed_top/reply_me/controller.dart @@ -1,45 +1,44 @@ -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:get/get.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/pages/common/common_controller.dart'; +import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/http/msg.dart'; import '../../../models/msg/msgfeed_reply_me.dart'; -class ReplyMeController extends GetxController { - RxList msgFeedReplyMeList = [].obs; - bool isLoading = false; +class ReplyMeController extends CommonController { int cursor = -1; int cursorTime = -1; - bool isEnd = false; - Future queryMsgFeedReplyMe() async { - if (isLoading) return; - isLoading = true; - var res = - await MsgHttp.msgFeedReplyMe(cursor: cursor, cursorTime: cursorTime); - isLoading = false; - if (res['status']) { - MsgFeedReplyMe data = MsgFeedReplyMe.fromJson(res['data']); - isEnd = data.cursor?.isEnd ?? false; - if (cursor == -1) { - msgFeedReplyMeList.assignAll(data.items!); - } else { - msgFeedReplyMeList.addAll(data.items!); - } - cursor = data.cursor?.id ?? -1; - cursorTime = data.cursor?.time ?? -1; - } else { - SmartDialog.showToast(res['msg']); + @override + void onInit() { + super.onInit(); + queryData(); + } + + @override + bool customHandleResponse(Success response) { + MsgFeedReplyMe data = response.response; + if (data.cursor?.isEnd == true || data.items.isNullOrEmpty) { + isEnd = true; } + cursor = data.cursor?.id ?? -1; + cursorTime = data.cursor?.time ?? -1; + if (currentPage != 1 && loadingState.value is Success) { + data.items ??= []; + data.items!.insert(0, (loadingState.value as Success).response); + } + loadingState.value = LoadingState.success(data.items); + return true; } - Future onLoad() async { - if (isEnd) return; - queryMsgFeedReplyMe(); - } - - Future onRefresh() async { + @override + Future onRefresh() { cursor = -1; cursorTime = -1; - queryMsgFeedReplyMe(); + return super.onRefresh(); } + + @override + Future customGetData() => + MsgHttp.msgFeedReplyMe(cursor: cursor, cursorTime: cursorTime); } diff --git a/lib/pages/msg_feed_top/reply_me/view.dart b/lib/pages/msg_feed_top/reply_me/view.dart index 89790027..61955a9d 100644 --- a/lib/pages/msg_feed_top/reply_me/view.dart +++ b/lib/pages/msg_feed_top/reply_me/view.dart @@ -1,5 +1,6 @@ +import 'package:PiliPlus/common/widgets/loading_widget.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; -import 'package:easy_debounce/easy_throttle.dart'; +import 'package:PiliPlus/http/loading_state.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:PiliPlus/common/widgets/network_img_layer.dart'; @@ -15,33 +16,7 @@ class ReplyMePage extends StatefulWidget { } class _ReplyMePageState extends State { - late final ReplyMeController _replyMeController = - Get.put(ReplyMeController()); - final ScrollController _scrollController = ScrollController(); - - @override - void initState() { - _replyMeController.queryMsgFeedReplyMe(); - super.initState(); - _scrollController.addListener(_scrollListener); - } - - @override - void dispose() { - _scrollController.removeListener(_scrollListener); - _scrollController.dispose(); - super.dispose(); - } - - Future _scrollListener() async { - if (_scrollController.position.pixels >= - _scrollController.position.maxScrollExtent - 200) { - EasyThrottle.throttle('my-throttler', const Duration(milliseconds: 800), - () async { - await _replyMeController.onLoad(); - }); - } - } + late final _replyMeController = Get.put(ReplyMeController()); @override Widget build(BuildContext context) { @@ -51,37 +26,42 @@ class _ReplyMePageState extends State { onRefresh: () async { await _replyMeController.onRefresh(); }, - // TODO: refactor - child: Obx( - () { - if (_replyMeController.msgFeedReplyMeList.isEmpty) { - return const Center( - child: CircularProgressIndicator(), - ); - } - return ListView.separated( - controller: _scrollController, - itemCount: _replyMeController.msgFeedReplyMeList.length, + child: Obx(() => _buildBody(_replyMeController.loadingState.value)), + ), + ); + } + + Widget _buildBody(LoadingState loadingState) { + return switch (loadingState) { + Loading() => loadingWidget, + Success() => (loadingState.response as List?)?.isNotEmpty == true + ? ListView.separated( + itemCount: loadingState.response.length, physics: const AlwaysScrollableScrollPhysics(), - itemBuilder: (context, int i) { + padding: EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom + 80), + itemBuilder: (context, int index) { + if (index == loadingState.response.length - 1) { + _replyMeController.onLoadMore(); + } + return ListTile( onTap: () { - String? nativeUri = _replyMeController - .msgFeedReplyMeList[i].item?.nativeUri; + String? nativeUri = + loadingState.response[index].item?.nativeUri; if (nativeUri != null) { PiliScheme.routePushFromUrl(nativeUri); } - // SmartDialog.showToast("跳转至:$nativeUri(暂未实现)"); }, leading: NetworkImgLayer( width: 45, height: 45, type: 'avatar', - src: _replyMeController.msgFeedReplyMeList[i].user?.avatar, + src: loadingState.response[index].user?.avatar, ), title: Text( - "${_replyMeController.msgFeedReplyMeList[i].user?.nickname} " - "回复了我的${_replyMeController.msgFeedReplyMeList[i].item?.business}", + "${loadingState.response[index].user?.nickname} " + "回复了我的${loadingState.response[index].item?.business}", style: Theme.of(context) .textTheme .bodyMedium! @@ -93,19 +73,18 @@ class _ReplyMePageState extends State { children: [ const SizedBox(height: 4), Text( - _replyMeController.msgFeedReplyMeList[i].item - ?.sourceContent ?? + loadingState.response[index].item?.sourceContent ?? "", style: Theme.of(context).textTheme.bodyMedium), const SizedBox(height: 4), - if (_replyMeController.msgFeedReplyMeList[i].item - ?.targetReplyContent != + if (loadingState + .response[index].item?.targetReplyContent != null && - _replyMeController.msgFeedReplyMeList[i].item - ?.targetReplyContent != + loadingState + .response[index].item?.targetReplyContent != "") Text( - "| ${_replyMeController.msgFeedReplyMeList[i].item?.targetReplyContent}", + "| ${loadingState.response[index].item?.targetReplyContent}", maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context) @@ -115,23 +94,25 @@ class _ReplyMePageState extends State { color: Theme.of(context).colorScheme.outline, height: 1.5)), - if (_replyMeController.msgFeedReplyMeList[i].item - ?.rootReplyContent != + if (loadingState + .response[index].item?.rootReplyContent != null && - _replyMeController.msgFeedReplyMeList[i].item - ?.rootReplyContent != + loadingState + .response[index].item?.rootReplyContent != "") Text( - " | ${_replyMeController.msgFeedReplyMeList[i].item?.rootReplyContent}", + " | ${loadingState.response[index].item?.rootReplyContent}", maxLines: 1, overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .labelMedium! - .copyWith( - color: - Theme.of(context).colorScheme.outline, - height: 1.5)), + style: + Theme.of(context) + .textTheme + .labelMedium! + .copyWith( + color: Theme.of(context) + .colorScheme + .outline, + height: 1.5)), ]), ); }, @@ -143,10 +124,13 @@ class _ReplyMePageState extends State { color: Colors.grey.withOpacity(0.1), ); }, - ); - }, + ) + : scrollErrorWidget(callback: _replyMeController.onReload), + Error() => scrollErrorWidget( + errMsg: loadingState.errMsg, + callback: _replyMeController.onReload, ), - ), - ); + LoadingState() => throw UnimplementedError(), + }; } } diff --git a/lib/pages/msg_feed_top/sys_msg/controller.dart b/lib/pages/msg_feed_top/sys_msg/controller.dart index c06fe2b0..cca6c149 100644 --- a/lib/pages/msg_feed_top/sys_msg/controller.dart +++ b/lib/pages/msg_feed_top/sys_msg/controller.dart @@ -1,38 +1,27 @@ +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/pages/common/common_controller.dart'; +import 'package:PiliPlus/utils/extension.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:get/get.dart'; import 'package:PiliPlus/http/msg.dart'; -import '../../../models/msg/msgfeed_sys_msg.dart'; - -class SysMsgController extends GetxController { - static const pageSize = 20; - RxList msgFeedSysMsgList = [].obs; - bool isLoading = false; +class SysMsgController extends CommonController { + final pageSize = 20; int cursor = -1; - bool isEnd = false; - Future queryMsgFeedSysMsg() async { - if (isLoading) return; - isLoading = true; - final res = await MsgHttp.msgFeedNotify(cursor: cursor, pageSize: pageSize); - isLoading = false; - if (res['status']) { - final data = (res['data'] as List) - .map((i) => SystemNotifyList.fromJson(i)) - .toList(); - isEnd = data.length + 1 < pageSize; // data.length会比pageSize小1 - if (data.isNotEmpty) { - if (cursor == -1) { - msgFeedSysMsgList.assignAll(data); - } else { - msgFeedSysMsgList.addAll(data); - } - cursor = data.last.cursor ?? -1; - msgSysUpdateCursor(msgFeedSysMsgList.first.cursor!); - } - } else { - SmartDialog.showToast(res['msg']); + @override + void onInit() { + super.onInit(); + queryData(); + } + + @override + List? handleListResponse(List currentList, List dataList) { + cursor = dataList.last.cursor ?? -1; + msgSysUpdateCursor(dataList.first.cursor!); + if (isEnd.not && dataList.length + 1 < pageSize) { + isEnd = true; } + return null; } Future msgSysUpdateCursor(int cursor) async { @@ -46,23 +35,27 @@ class SysMsgController extends GetxController { } } - Future onLoad() async { - if (isEnd) return; - queryMsgFeedSysMsg(); - } - - Future onRefresh() async { + @override + Future onRefresh() { cursor = -1; - queryMsgFeedSysMsg(); + return super.onRefresh(); } - Future onRemove(int index) async { - var res = await MsgHttp.removeSysMsg(msgFeedSysMsgList[index].id); - if (res['status']) { - msgFeedSysMsgList.removeAt(index); - SmartDialog.showToast('删除成功'); - } else { - SmartDialog.showToast(res['msg']); - } + Future onRemove(dynamic id, int index) async { + try { + var res = await MsgHttp.removeSysMsg(id); + if (res['status']) { + List list = (loadingState.value as Success).response; + list.removeAt(index); + loadingState.value = LoadingState.success(list); + SmartDialog.showToast('删除成功'); + } else { + SmartDialog.showToast(res['msg']); + } + } catch (_) {} } + + @override + Future customGetData() => + MsgHttp.msgFeedNotify(cursor: cursor, pageSize: pageSize); } diff --git a/lib/pages/msg_feed_top/sys_msg/view.dart b/lib/pages/msg_feed_top/sys_msg/view.dart index 6c506a6d..70fb2db1 100644 --- a/lib/pages/msg_feed_top/sys_msg/view.dart +++ b/lib/pages/msg_feed_top/sys_msg/view.dart @@ -1,10 +1,11 @@ import 'dart:convert'; +import 'package:PiliPlus/common/widgets/loading_widget.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -20,32 +21,7 @@ class SysMsgPage extends StatefulWidget { } class _SysMsgPageState extends State { - late final SysMsgController _sysMsgController = Get.put(SysMsgController()); - final ScrollController _scrollController = ScrollController(); - - @override - void initState() { - _sysMsgController.queryMsgFeedSysMsg(); - super.initState(); - _scrollController.addListener(_scrollListener); - } - - @override - void dispose() { - _scrollController.removeListener(_scrollListener); - _scrollController.dispose(); - super.dispose(); - } - - Future _scrollListener() async { - if (_scrollController.position.pixels >= - _scrollController.position.maxScrollExtent - 200) { - EasyThrottle.throttle('my-throttler', const Duration(milliseconds: 800), - () async { - await _sysMsgController.onLoad(); - }); - } - } + late final _sysMsgController = Get.put(SysMsgController()); @override Widget build(BuildContext context) { @@ -57,21 +33,26 @@ class _SysMsgPageState extends State { onRefresh: () async { await _sysMsgController.onRefresh(); }, - // TODO: refactor - child: Obx( - () { - if (_sysMsgController.msgFeedSysMsgList.isEmpty) { - return const Center( - child: CircularProgressIndicator(), - ); - } - return ListView.separated( - controller: _scrollController, - itemCount: _sysMsgController.msgFeedSysMsgList.length, + child: Obx(() => _buildBody(_sysMsgController.loadingState.value)), + ), + ); + } + + Widget _buildBody(LoadingState loadingState) { + return switch (loadingState) { + Loading() => loadingWidget, + Success() => (loadingState.response as List?)?.isNotEmpty == true + ? ListView.separated( + itemCount: loadingState.response.length, physics: const AlwaysScrollableScrollPhysics(), - itemBuilder: (context, int i) { - String? content = - _sysMsgController.msgFeedSysMsgList[i].content; + padding: EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom + 80), + itemBuilder: (context, int index) { + if (index == loadingState.response.length - 1) { + _sysMsgController.onLoadMore(); + } + + String? content = loadingState.response[index].content; if (content != null) { try { dynamic jsonContent = json.decode(content); @@ -101,7 +82,10 @@ class _SysMsgPageState extends State { TextButton( onPressed: () { Get.back(); - _sysMsgController.onRemove(i); + _sysMsgController.onRemove( + loadingState.response[index].id, + index, + ); }, child: const Text('确定'), ), @@ -109,7 +93,7 @@ class _SysMsgPageState extends State { )); }, title: Text( - "${_sysMsgController.msgFeedSysMsgList[i].title}", + "${loadingState.response[index].title}", style: Theme.of(context).textTheme.titleMedium, ), subtitle: Column( @@ -130,7 +114,7 @@ class _SysMsgPageState extends State { SizedBox( width: double.infinity, child: Text( - "${_sysMsgController.msgFeedSysMsgList[i].timeAt}", + "${loadingState.response[index].timeAt}", maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context) @@ -154,11 +138,14 @@ class _SysMsgPageState extends State { color: Colors.grey.withOpacity(0.1), ); }, - ); - }, + ) + : scrollErrorWidget(callback: _sysMsgController.onReload), + Error() => scrollErrorWidget( + errMsg: loadingState.errMsg, + callback: _sysMsgController.onReload, ), - ), - ); + LoadingState() => throw UnimplementedError(), + }; } InlineSpan _buildContent(String content) { diff --git a/lib/pages/video/detail/note/note_list_page.dart b/lib/pages/video/detail/note/note_list_page.dart index 24368446..1db51d68 100644 --- a/lib/pages/video/detail/note/note_list_page.dart +++ b/lib/pages/video/detail/note/note_list_page.dart @@ -86,6 +86,7 @@ class _NoteListPageState extends State { }, child: CustomScrollView( controller: ScrollController(), + physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverList.separated( itemBuilder: (context, index) {