diff --git a/lib/common/widgets/self_sized_horizontal_list.dart b/lib/common/widgets/self_sized_horizontal_list.dart index 673fb4708..a4b4f1153 100644 --- a/lib/common/widgets/self_sized_horizontal_list.dart +++ b/lib/common/widgets/self_sized_horizontal_list.dart @@ -35,6 +35,14 @@ class _SelfSizedHorizontalListState extends State { bool get isInit => height == null; + // @override + // void didUpdateWidget(SelfSizedHorizontalList oldWidget) { + // super.didUpdateWidget(oldWidget); + // if (BuildConfig.isDebug) { + // prevHeight = null; + // } + // } + @override Widget build(BuildContext context) { if (height == null) { diff --git a/lib/http/api.dart b/lib/http/api.dart index df18bffb5..d82f5fad6 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -807,4 +807,16 @@ class Api { '${HttpString.liveBaseUrl}/xlive/app-interface/v2/second/getList'; static const String msgSetNotice = '/x/msgfeed/notice'; + + static const String liveAreaList = + '${HttpString.liveBaseUrl}/xlive/app-interface/v2/index/getAreaList'; + + static const String liveRoomAreaList = + '${HttpString.liveBaseUrl}/room/v1/Area/getList'; + + static const String getLiveFavTag = + '${HttpString.liveBaseUrl}/xlive/app-interface/v2/second/get_fav_tag'; + + static const String setLiveFavTag = + '${HttpString.liveBaseUrl}/xlive/app-interface/v2/second/set_fav_tag'; } diff --git a/lib/http/live.dart b/lib/http/live.dart index 12ec3f4da..15222174c 100644 --- a/lib/http/live.dart +++ b/lib/http/live.dart @@ -2,6 +2,8 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/live/live_area_list/area_item.dart'; +import 'package:PiliPlus/models/live/live_area_list/area_list.dart'; import 'package:PiliPlus/models/live/live_emoticons/data.dart'; import 'package:PiliPlus/models/live/live_emoticons/datum.dart'; import 'package:PiliPlus/models/live/live_feed_index/data.dart'; @@ -234,8 +236,9 @@ class LiveHttp { static Future> liveSecondList({ required int pn, required bool isLogin, - required int? areaId, - required int? parentAreaId, + required areaId, + required parentAreaId, + String? sortType, }) async { final params = { if (isLogin) 'access_key': Accounts.main.accessKey, @@ -258,6 +261,7 @@ class LiveHttp { 'page_size': '20', 'platform': 'android', 'qn': '0', + if (sortType != null) 'sort_type': sortType, 'tag_version': '1', 's_locale': 'zh_CN', 'scale': '2', @@ -279,4 +283,153 @@ class LiveHttp { return LoadingState.error(res.data['message']); } } + + static Future?>> liveAreaList({ + required bool isLogin, + }) async { + final params = { + if (isLogin) 'access_key': Accounts.main.accessKey, + 'appkey': Constants.appKey, + 'actionKey': 'appkey', + 'build': '8350200', + 'c_locale': 'zh_CN', + 'device': 'pad', + 'disable_rcmd': '0', + 'mobi_app': 'android_hd', + 'platform': 'android', + 's_locale': 'zh_CN', + 'statistics': Constants.statistics, + 'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000, + }; + Utils.appSign( + params, + Constants.appKey, + Constants.appSec, + ); + var res = await Request().get( + Api.liveAreaList, + queryParameters: params, + ); + if (res.data['code'] == 0) { + return LoadingState.success((res.data['data']?['list'] as List?) + ?.map((e) => AreaList.fromJson(e)) + .toList()); + } else { + return LoadingState.error(res.data['message']); + } + } + + static Future>> getLiveFavTag({ + required bool isLogin, + }) async { + final params = { + if (isLogin) 'access_key': Accounts.main.accessKey, + 'appkey': Constants.appKey, + 'actionKey': 'appkey', + 'build': '8350200', + 'c_locale': 'zh_CN', + 'device': 'pad', + 'disable_rcmd': '0', + 'mobi_app': 'android_hd', + 'platform': 'android', + 's_locale': 'zh_CN', + 'statistics': Constants.statistics, + 'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000, + }; + Utils.appSign( + params, + Constants.appKey, + Constants.appSec, + ); + var res = await Request().get( + Api.getLiveFavTag, + queryParameters: params, + ); + + if (res.data['code'] == 0) { + return LoadingState.success((res.data['data']?['tags'] as List?) + ?.map((e) => AreaItem.fromJson(e)) + .toList() ?? + []); + } else { + return LoadingState.error(res.data['message']); + } + } + + static Future setLiveFavTag({ + required List ids, + }) async { + final data = { + 'tags': ids.join(','), + 'access_key': Accounts.main.accessKey, + 'appkey': Constants.appKey, + 'actionKey': 'appkey', + 'build': '8350200', + 'c_locale': 'zh_CN', + 'device': 'pad', + 'disable_rcmd': '0', + 'mobi_app': 'android_hd', + 'platform': 'android', + 's_locale': 'zh_CN', + 'statistics': Constants.statistics, + 'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000, + }; + Utils.appSign( + data, + Constants.appKey, + Constants.appSec, + ); + var res = await Request().post( + Api.setLiveFavTag, + data: data, + options: Options( + contentType: Headers.formUrlEncodedContentType, + ), + ); + + if (res.data['code'] == 0) { + return {'status': true}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } + + static Future?>> liveRoomAreaList({ + required bool isLogin, + required parentid, + }) async { + final params = { + if (isLogin) 'access_key': Accounts.main.accessKey, + 'appkey': Constants.appKey, + 'actionKey': 'appkey', + 'build': '8350200', + 'c_locale': 'zh_CN', + 'device': 'pad', + 'disable_rcmd': '0', + 'need_entrance': 1, + 'parent_id': parentid, + 'source_id': 2, + 'mobi_app': 'android_hd', + 'platform': 'android', + 's_locale': 'zh_CN', + 'statistics': Constants.statistics, + 'ts': DateTime.now().millisecondsSinceEpoch ~/ 1000, + }; + Utils.appSign( + params, + Constants.appKey, + Constants.appSec, + ); + var res = await Request().get( + Api.liveRoomAreaList, + queryParameters: params, + ); + if (res.data['code'] == 0) { + return LoadingState.success((res.data['data'] as List?) + ?.map((e) => AreaItem.fromJson(e)) + .toList()); + } else { + return LoadingState.error(res.data['message']); + } + } } diff --git a/lib/models/live/live_area_list/area_item.dart b/lib/models/live/live_area_list/area_item.dart new file mode 100644 index 000000000..b8e1abf39 --- /dev/null +++ b/lib/models/live/live_area_list/area_item.dart @@ -0,0 +1,48 @@ +class AreaItem { + dynamic id; + String? name; + String? link; + String? pic; + dynamic parentId; + String? parentName; + int? areaType; + int? tagType; + + bool? isFav; + + AreaItem({ + this.id, + this.name, + this.link, + this.pic, + this.parentId, + this.parentName, + this.areaType, + this.tagType, + }); + + factory AreaItem.fromJson(Map json) => AreaItem( + id: json['id'], + name: json['name'] as String?, + link: json['link'] as String?, + pic: json['pic'] as String?, + parentId: json['parent_id'], + parentName: json['parent_name'] as String?, + areaType: json['area_type'] as int?, + tagType: json['tag_type'] as int?, + ); + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other is AreaItem) { + return id == other.id && parentId == other.parentId; + } + return false; + } + + @override + int get hashCode => Object.hash(id, parentId); +} diff --git a/lib/models/live/live_area_list/area_list.dart b/lib/models/live/live_area_list/area_list.dart new file mode 100644 index 000000000..30b25a011 --- /dev/null +++ b/lib/models/live/live_area_list/area_list.dart @@ -0,0 +1,19 @@ +import 'area_item.dart'; + +class AreaList { + int? id; + String? name; + int? parentAreaType; + List? areaList; + + AreaList({this.id, this.name, this.parentAreaType, this.areaList}); + + factory AreaList.fromJson(Map json) => AreaList( + id: json['id'] as int?, + name: json['name'] ?? '', + parentAreaType: json['parent_area_type'] as int?, + areaList: (json['area_list'] as List?) + ?.map((e) => AreaItem.fromJson(e as Map)) + .toList(), + ); +} diff --git a/lib/models/live/live_second_list/data.dart b/lib/models/live/live_second_list/data.dart index 799668c89..417605320 100644 --- a/lib/models/live/live_second_list/data.dart +++ b/lib/models/live/live_second_list/data.dart @@ -1,12 +1,15 @@ import 'package:PiliPlus/models/live/live_feed_index/card_data_list_item.dart'; +import 'package:PiliPlus/models/live/live_second_list/tag.dart'; class LiveSecondData { int? count; List? cardList; + List? newTags; LiveSecondData({ this.count, this.cardList, + this.newTags, }); factory LiveSecondData.fromJson(Map json) => LiveSecondData( @@ -14,10 +17,8 @@ class LiveSecondData { cardList: (json['list'] as List?) ?.map((e) => CardLiveItem.fromJson(e as Map)) .toList(), + newTags: (json['new_tags'] as List?) + ?.map((e) => LiveSecondTag.fromJson(e as Map)) + .toList(), ); - - Map toJson() => { - 'count': count, - 'list': cardList?.map((e) => e.toJson()).toList(), - }; } diff --git a/lib/models/live/live_second_list/tag.dart b/lib/models/live/live_second_list/tag.dart new file mode 100644 index 000000000..e617c12c9 --- /dev/null +++ b/lib/models/live/live_second_list/tag.dart @@ -0,0 +1,17 @@ +class LiveSecondTag { + int? id; + String? name; + String? sortType; + + LiveSecondTag({ + this.id, + this.name, + this.sortType, + }); + + factory LiveSecondTag.fromJson(Map json) => LiveSecondTag( + id: json['id'], + name: json['name'], + sortType: json['sort_type'], + ); +} diff --git a/lib/pages/live/controller.dart b/lib/pages/live/controller.dart index bfe06e8b5..1d0e3e5a1 100644 --- a/lib/pages/live/controller.dart +++ b/lib/pages/live/controller.dart @@ -3,6 +3,7 @@ import 'package:PiliPlus/http/live.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/live/live_feed_index/card_data_list_item.dart'; import 'package:PiliPlus/models/live/live_feed_index/card_list.dart'; +import 'package:PiliPlus/models/live/live_second_list/tag.dart'; import 'package:PiliPlus/pages/common/common_list_controller.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:get/get_rx/src/rx_types/rx_types.dart'; @@ -14,9 +15,16 @@ class LiveController extends CommonListController { queryData(); } + // area int? areaId; + String? sortType; int? parentAreaId; final RxInt areaIndex = 0.obs; + + // tag + final RxInt tagIndex = 0.obs; + List? newTags; + final Rx> topState = Pair(first: null, second: null).obs; final RxBool isLogin = Accounts.main.isLogin.obs; @@ -28,11 +36,19 @@ class LiveController extends CommonListController { @override bool customHandleResponse(bool isRefresh, Success response) { - if (isRefresh && areaIndex.value == 0) { - topState.value = Pair( - first: response.response.followItem, - second: response.response.areaItem, - ); + if (isRefresh) { + if (areaIndex.value == 0) { + topState.value = Pair( + first: response.response.followItem, + second: response.response.areaItem, + ); + } else { + newTags = response.response.newTags; + if (sortType != null) { + tagIndex.value = + newTags?.indexWhere((e) => e.sortType == sortType) ?? -1; + } + } } return false; } @@ -45,6 +61,7 @@ class LiveController extends CommonListController { isLogin: isLogin.value, areaId: areaId, parentAreaId: parentAreaId, + sortType: sortType, ); } return LiveHttp.liveFeedIndex(pn: currentPage, isLogin: isLogin.value); @@ -72,6 +89,12 @@ class LiveController extends CommonListController { first: data.followItem, second: data.areaItem, ); + areaIndex.value = (data.areaItem?.cardData?.areaEntranceV3?.list + ?.indexWhere((e) => + e.areaV2Id == areaId && + e.areaV2ParentId == parentAreaId) ?? + -2) + + 1; } } @@ -79,6 +102,9 @@ class LiveController extends CommonListController { if (index == areaIndex.value) { return; } + tagIndex.value = 0; + newTags = null; + sortType = null; areaIndex.value = index; areaId = cardLiveItem?.areaV2Id; parentAreaId = cardLiveItem?.areaV2ParentId; @@ -87,4 +113,13 @@ class LiveController extends CommonListController { isEnd = false; queryData(); } + + void onSelectTag(int index, String? sortType) { + tagIndex.value = index; + this.sortType = sortType; + + currentPage = 1; + isEnd = false; + queryData(); + } } diff --git a/lib/pages/live/view.dart b/lib/pages/live/view.dart index f2a9293ed..09aaafa53 100644 --- a/lib/pages/live/view.dart +++ b/lib/pages/live/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/skeleton/video_card_v.dart'; +import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; @@ -11,6 +12,7 @@ import 'package:PiliPlus/models/live/live_feed_index/card_list.dart'; import 'package:PiliPlus/pages/common/common_page.dart'; import 'package:PiliPlus/pages/live/controller.dart'; import 'package:PiliPlus/pages/live/widgets/live_item_app.dart'; +import 'package:PiliPlus/pages/live_area/view.dart'; import 'package:PiliPlus/pages/live_follow/view.dart'; import 'package:PiliPlus/pages/search/widgets/search_text.dart'; import 'package:PiliPlus/utils/grid.dart'; @@ -73,39 +75,60 @@ class _LivePageState extends CommonPageState SliverToBoxAdapter(child: _buildFollowList(data.first!)), if (data.second?.cardData?.areaEntranceV3?.list?.isNotEmpty == true) SliverToBoxAdapter( - child: SelfSizedHorizontalList( - gapSize: 12, - padding: const EdgeInsets.only(bottom: 10), - childBuilder: (index) { - late final item = - data.second!.cardData!.areaEntranceV3!.list![index - 1]; - return Obx( - () => SearchText( - fontSize: 14, - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 3, - ), - text: index == 0 ? '推荐' : '${item.title}', - bgColor: index == controller.areaIndex.value - ? Theme.of(context).colorScheme.secondaryContainer - : Colors.transparent, - textColor: index == controller.areaIndex.value - ? Theme.of(context).colorScheme.onSecondaryContainer - : null, - onTap: (value) { - controller.onSelectArea( - index, - index == 0 ? null : item, + child: Row( + children: [ + Expanded( + child: SelfSizedHorizontalList( + gapSize: 12, + padding: + const EdgeInsets.only(top: 10, bottom: 10, right: 12), + childBuilder: (index) { + late final item = data + .second!.cardData!.areaEntranceV3!.list![index - 1]; + return Obx( + () => SearchText( + fontSize: 14, + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 3, + ), + text: index == 0 ? '推荐' : '${item.title}', + bgColor: index == controller.areaIndex.value + ? Theme.of(context).colorScheme.secondaryContainer + : Colors.transparent, + textColor: index == controller.areaIndex.value + ? Theme.of(context) + .colorScheme + .onSecondaryContainer + : null, + onTap: (value) { + controller.onSelectArea( + index, + index == 0 ? null : item, + ); + }, + ), ); }, + itemCount: + data.second!.cardData!.areaEntranceV3!.list!.length + 1, ), - ); - }, - itemCount: - data.second!.cardData!.areaEntranceV3!.list!.length + 1, + ), + iconButton( + size: 26, + iconSize: 16, + context: context, + tooltip: '全部标签', + icon: Icons.widgets, + onPressed: () { + Get.to(const LiveAreaPage()); + }, + ), + ], ), - ), + ) + else + const SliverToBoxAdapter(child: SizedBox(height: 10)), ], ); } @@ -128,29 +151,70 @@ class _LivePageState extends CommonPageState ), ), Success() => loadingState.response?.isNotEmpty == true - ? SliverGrid( - gridDelegate: SliverGridDelegateWithExtentAndRatio( - mainAxisSpacing: StyleString.cardSpace, - crossAxisSpacing: StyleString.cardSpace, - maxCrossAxisExtent: Grid.smallCardWidth, - childAspectRatio: StyleString.aspectRatio, - mainAxisExtent: MediaQuery.textScalerOf(context).scale(90), - ), - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index == loadingState.response!.length - 1) { - controller.onLoadMore(); - } - final item = loadingState.response![index]; - if (item is LiveCardList) { - return LiveCardVApp( - item: item.cardData!.smallCardV1!, - ); - } - return LiveCardVApp(item: item); - }, - childCount: loadingState.response!.length, - ), + ? SliverMainAxisGroup( + slivers: [ + if (controller.newTags?.isNotEmpty == true) + SliverToBoxAdapter( + child: SelfSizedHorizontalList( + gapSize: 12, + padding: const EdgeInsets.only(bottom: 8), + childBuilder: (index) { + late final item = controller.newTags![index]; + return Obx( + () => SearchText( + fontSize: 13, + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 3, + ), + text: '${item.name}', + bgColor: index == controller.tagIndex.value + ? Theme.of(context) + .colorScheme + .secondaryContainer + : Colors.transparent, + textColor: index == controller.tagIndex.value + ? Theme.of(context) + .colorScheme + .onSecondaryContainer + : null, + onTap: (value) { + controller.onSelectTag( + index, + item.sortType, + ); + }, + ), + ); + }, + itemCount: controller.newTags!.length, + ), + ), + SliverGrid( + gridDelegate: SliverGridDelegateWithExtentAndRatio( + mainAxisSpacing: StyleString.cardSpace, + crossAxisSpacing: StyleString.cardSpace, + maxCrossAxisExtent: Grid.smallCardWidth, + childAspectRatio: StyleString.aspectRatio, + mainAxisExtent: MediaQuery.textScalerOf(context).scale(90), + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == loadingState.response!.length - 1) { + controller.onLoadMore(); + } + final item = loadingState.response![index]; + if (item is LiveCardList) { + return LiveCardVApp( + item: item.cardData!.smallCardV1!, + ); + } + return LiveCardVApp(item: item); + }, + childCount: loadingState.response!.length, + ), + ), + ], ) : HttpError(onReload: controller.onReload), Error() => HttpError( @@ -216,7 +280,6 @@ class _LivePageState extends CommonPageState ), if (item.cardData?.myIdolV1?.list?.isNotEmpty == true) _buildFollowBody(theme, item.cardData!.myIdolV1!.list!), - const SizedBox(height: 10), ], ); } diff --git a/lib/pages/live_area/controller.dart b/lib/pages/live_area/controller.dart new file mode 100644 index 000000000..3cc2beaf6 --- /dev/null +++ b/lib/pages/live_area/controller.dart @@ -0,0 +1,66 @@ +import 'package:PiliPlus/http/live.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/live/live_area_list/area_item.dart'; +import 'package:PiliPlus/models/live/live_area_list/area_list.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; +import 'package:PiliPlus/utils/storage.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class LiveAreaController + extends CommonListController?, AreaList> { + final isLogin = Accounts.main.isLogin; + + late final isEditing = false.obs; + + @override + void onInit() { + super.onInit(); + if (isLogin) { + queryFavTags(); + } + queryData(); + } + + @override + Future onRefresh() { + if (isLogin) { + queryFavTags(); + } + return super.onRefresh(); + } + + Rx>> favState = + LoadingState>.loading().obs; + + @override + Future?>> customGetData() => + LiveHttp.liveAreaList(isLogin: isLogin); + + Future queryFavTags() async { + favState.value = await LiveHttp.getLiveFavTag(isLogin: isLogin); + } + + Future setFavTag() async { + if (favState.value is Success) { + final res = await LiveHttp.setLiveFavTag( + ids: favState.value.data.map((e) => e.id).toList()); + if (res['status']) { + isEditing.value = !isEditing.value; + SmartDialog.showToast('设置成功'); + } else { + SmartDialog.showToast(res['msg']); + } + } else { + isEditing.value = !isEditing.value; + } + } + + void onEdit() { + if (isEditing.value) { + setFavTag(); + } else { + isEditing.value = !isEditing.value; + } + } +} diff --git a/lib/pages/live_area/view.dart b/lib/pages/live_area/view.dart new file mode 100644 index 000000000..499014378 --- /dev/null +++ b/lib/pages/live_area/view.dart @@ -0,0 +1,372 @@ +import 'package:PiliPlus/common/widgets/button/icon_button.dart'; +import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/live/live_area_list/area_item.dart'; +import 'package:PiliPlus/models/live/live_area_list/area_list.dart'; +import 'package:PiliPlus/pages/live_area/controller.dart'; +import 'package:PiliPlus/pages/live_area_detail/view.dart'; +import 'package:PiliPlus/pages/search/widgets/search_text.dart'; +import 'package:PiliPlus/utils/extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_sortable_wrap/sortable_wrap.dart'; +import 'package:get/get.dart'; +import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; + +class LiveAreaPage extends StatefulWidget { + const LiveAreaPage({super.key}); + + @override + State createState() => _LiveAreaPageState(); +} + +class _LiveAreaPageState extends State { + final _controller = Get.put(LiveAreaController()); + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + title: const Text('全部标签'), + actions: _controller.isLogin + ? [ + TextButton( + onPressed: _controller.onEdit, + style: TextButton.styleFrom( + visualDensity: VisualDensity.compact, + ), + child: Obx( + () => Text(_controller.isEditing.value ? '完成' : '编辑')), + ), + const SizedBox(width: 16), + ] + : null, + ), + body: SafeArea( + top: false, + bottom: false, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_controller.isLogin) + Obx(() => _buildFavWidget(theme, _controller.favState.value)), + Expanded( + child: + Obx(() => _buildBody(theme, _controller.loadingState.value)), + ), + ], + ), + ), + ); + } + + Widget _buildBody( + ThemeData theme, LoadingState?> loadingState) { + return switch (loadingState) { + Loading() => const SizedBox.shrink(), + Success() => loadingState.response?.isNotEmpty == true + ? DefaultTabController( + length: loadingState.response!.length, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TabBar( + isScrollable: true, + tabAlignment: TabAlignment.start, + tabs: loadingState.response! + .map((e) => Tab(text: e.name)) + .toList(), + ), + Expanded( + child: tabBarView( + children: loadingState.response! + .map( + (e) => KeepAliveWrapper( + builder: (context) { + if (e.areaList.isNullOrEmpty) { + return const SizedBox.shrink(); + } + return GridView.builder( + padding: EdgeInsets.only( + top: 12, + bottom: + MediaQuery.paddingOf(context).bottom + + 80, + ), + gridDelegate: + const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 100, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + mainAxisExtent: 80, + ), + itemCount: e.areaList!.length, + itemBuilder: (context, index) { + final item = e.areaList![index]; + return _tagItem( + theme: theme, + item: item, + onPressed: () { + // success + if (item.isFav == true) { + _controller.favState + ..value.data.remove(item) + ..refresh(); + item.isFav = false; + (context as Element) + .markNeedsBuild(); + } else { + // check + if (_controller.favState.value + is Success) { + _controller.favState + ..value.data.add(item) + ..refresh(); + item.isFav = true; + (context as Element) + .markNeedsBuild(); + } + } + }, + ); + }, + ); + }, + ), + ) + .toList()), + ) + ], + ), + ) + : scrollErrorWidget(onReload: _controller.onReload), + Error() => scrollErrorWidget( + errMsg: loadingState.errMsg, + onReload: _controller.onReload, + ), + }; + } + + Widget _buildFavWidget( + ThemeData theme, LoadingState?> loadingState) { + if (loadingState is Success) { + final List? list = loadingState.data; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich( + TextSpan( + children: [ + const TextSpan(text: '我的常用标签 '), + TextSpan( + text: '点击进入标签', + style: TextStyle( + fontSize: 13, + color: theme.colorScheme.outline, + ), + ), + ], + ), + ), + const SizedBox(height: 8), + if (list?.isNotEmpty == true) ...[ + SortableWrap( + onSortStart: (index) { + _controller.isEditing.value = true; + }, + onSorted: (int oldIndex, int newIndex) { + list.insert(newIndex, list.removeAt(oldIndex)); + }, + spacing: 12, + runSpacing: 8, + children: list! + .map( + (item) => _favTagItem( + theme: theme, + item: item, + onPressed: () { + list.remove(item); + _controller.favState.refresh(); + + // update isFav + if (_controller.loadingState.value is Success) { + List? areaList = + _controller.loadingState.value.data; + if (areaList?.isNotEmpty == true) { + for (var i in areaList!) { + if (i.areaList?.isNotEmpty == true) { + for (var j in i.areaList!) { + if (j == item) { + j.isFav = false; + _controller.loadingState.refresh(); + break; + } + } + } + } + } + } + }, + ), + ) + .toList(), + ), + const SizedBox(height: 4), + ], + ], + ), + ); + } + + return const SizedBox.shrink(); + } + + Widget _tagItem({ + required ThemeData theme, + required AreaItem item, + required VoidCallback onPressed, + }) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + if (_controller.isEditing.value) { + onPressed(); + return; + } + + Get.to( + LiveAreaDetailPage( + areaId: item.id, + parentAreaId: item.parentId, + parentName: item.parentName ?? '', + ), + ); + }, + child: Stack( + alignment: Alignment.center, + clipBehavior: Clip.none, + children: [ + Column( + mainAxisSize: MainAxisSize.min, + children: [ + NetworkImgLayer( + width: 45, + height: 45, + src: item.pic, + type: 'emote', + ), + const SizedBox(height: 4), + Text( + item.name!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle(fontSize: 12), + ), + ], + ), + Positioned( + top: 0, + right: 16, + child: Obx(() { + if (_controller.isEditing.value && + _controller.favState.value is Success) { + // init isFav + item.isFav ??= _controller.favState.value.data.contains(item); + + return Builder( + builder: (context) { + return iconButton( + size: 17, + iconSize: 13, + context: context, + icon: item.isFav == true ? MdiIcons.check : MdiIcons.plus, + bgColor: item.isFav == true + ? theme.colorScheme.onInverseSurface + : null, + iconColor: + item.isFav == true ? theme.colorScheme.outline : null, + onPressed: onPressed, + ); + }, + ); + } + return const SizedBox.shrink(); + }), + ), + ], + ), + ); + } + + Widget _favTagItem({ + required ThemeData theme, + required AreaItem item, + required VoidCallback onPressed, + }) { + return Stack( + clipBehavior: Clip.none, + children: [ + DecoratedBox( + decoration: BoxDecoration( + border: Border.all( + color: theme.colorScheme.outline, + ), + borderRadius: const BorderRadius.all(Radius.circular(4)), + color: theme.colorScheme.surface, + ), + child: SearchText( + text: item.name!, + fontSize: 14, + bgColor: Colors.transparent, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 4, + ), + onTap: (value) { + if (_controller.isEditing.value) { + onPressed(); + return; + } + + Get.to( + LiveAreaDetailPage( + areaId: item.id, + parentAreaId: item.parentId, + parentName: item.parentName ?? '', + ), + ); + }, + ), + ), + Positioned( + right: -4, + top: -4, + child: Obx(() { + if (_controller.isEditing.value) { + final isDark = theme.brightness == Brightness.dark; + return iconButton( + size: 16, + iconSize: 12, + context: context, + icon: Icons.horizontal_rule, + bgColor: isDark + ? theme.colorScheme.error + : theme.colorScheme.errorContainer, + iconColor: isDark + ? theme.colorScheme.onError + : theme.colorScheme.onErrorContainer, + onPressed: onPressed, + ); + } + return const SizedBox.shrink(); + }), + ) + ], + ); + } +} diff --git a/lib/pages/live_area_detail/child/controller.dart b/lib/pages/live_area_detail/child/controller.dart new file mode 100644 index 000000000..c24eec20a --- /dev/null +++ b/lib/pages/live_area_detail/child/controller.dart @@ -0,0 +1,38 @@ +import 'package:PiliPlus/http/live.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/live/live_feed_index/card_data_list_item.dart'; +import 'package:PiliPlus/models/live/live_second_list/data.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; +import 'package:PiliPlus/utils/storage.dart'; + +class LiveAreaChildController + extends CommonListController { + LiveAreaChildController(this.areaId, this.parentAreaId); + final dynamic areaId; + final dynamic parentAreaId; + + String? sortType; + + final isLogin = Accounts.main.isLogin; + + @override + void onInit() { + super.onInit(); + queryData(); + } + + @override + List? getDataList(LiveSecondData response) { + return response.cardList; + } + + @override + Future> customGetData() => + LiveHttp.liveSecondList( + pn: currentPage, + isLogin: isLogin, + areaId: areaId, + parentAreaId: parentAreaId, + sortType: sortType, + ); +} diff --git a/lib/pages/live_area_detail/child/view.dart b/lib/pages/live_area_detail/child/view.dart new file mode 100644 index 000000000..5334d9997 --- /dev/null +++ b/lib/pages/live_area_detail/child/view.dart @@ -0,0 +1,103 @@ +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/skeleton/video_card_v.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/live/live_feed_index/card_data_list_item.dart'; +import 'package:PiliPlus/pages/live/widgets/live_item_app.dart'; +import 'package:PiliPlus/pages/live_area_detail/child/controller.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class LiveAreaChildPage extends StatefulWidget { + const LiveAreaChildPage({ + super.key, + required this.areaId, + required this.parentAreaId, + }); + + final dynamic areaId; + final dynamic parentAreaId; + + @override + State createState() => _LiveAreaChildPageState(); +} + +class _LiveAreaChildPageState extends State + with AutomaticKeepAliveClientMixin { + late final _controller = Get.put( + LiveAreaChildController(widget.areaId, widget.parentAreaId), + tag: '${widget.areaId}${widget.parentAreaId}', + ); + + @override + Widget build(BuildContext context) { + super.build(context); + return refreshIndicator( + onRefresh: _controller.onRefresh, + child: CustomScrollView( + slivers: [ + SliverPadding( + padding: EdgeInsets.only( + left: StyleString.safeSpace, + right: StyleString.safeSpace, + top: StyleString.safeSpace, + bottom: MediaQuery.paddingOf(context).bottom + 80, + ), + sliver: Obx(() => _buildBody(_controller.loadingState.value)), + ), + ], + ), + ); + } + + Widget _buildBody(LoadingState?> loadingState) { + return switch (loadingState) { + Loading() => SliverGrid( + gridDelegate: SliverGridDelegateWithExtentAndRatio( + mainAxisSpacing: StyleString.cardSpace, + crossAxisSpacing: StyleString.cardSpace, + maxCrossAxisExtent: Grid.smallCardWidth, + childAspectRatio: StyleString.aspectRatio, + mainAxisExtent: MediaQuery.textScalerOf(context).scale(90), + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + return const VideoCardVSkeleton(); + }, + childCount: 10, + ), + ), + Success() => loadingState.response?.isNotEmpty == true + ? SliverGrid( + gridDelegate: SliverGridDelegateWithExtentAndRatio( + mainAxisSpacing: StyleString.cardSpace, + crossAxisSpacing: StyleString.cardSpace, + maxCrossAxisExtent: Grid.smallCardWidth, + childAspectRatio: StyleString.aspectRatio, + mainAxisExtent: MediaQuery.textScalerOf(context).scale(90), + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == loadingState.response!.length - 1) { + _controller.onLoadMore(); + } + return LiveCardVApp(item: loadingState.response![index]); + }, + childCount: loadingState.response!.length, + ), + ) + : HttpError( + onReload: _controller.onReload, + ), + Error() => HttpError( + errMsg: loadingState.errMsg, + onReload: _controller.onReload, + ), + }; + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/pages/live_area_detail/controller.dart b/lib/pages/live_area_detail/controller.dart new file mode 100644 index 000000000..2d6ff7e78 --- /dev/null +++ b/lib/pages/live_area_detail/controller.dart @@ -0,0 +1,38 @@ +import 'dart:math'; + +import 'package:PiliPlus/http/live.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/live/live_area_list/area_item.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; +import 'package:PiliPlus/utils/storage.dart'; + +class LiveAreaDatailController + extends CommonListController?, AreaItem> { + LiveAreaDatailController(this.areaId, this.parentAreaId); + final dynamic areaId; + final dynamic parentAreaId; + + late int initialIndex = 0; + final isLogin = Accounts.main.isLogin; + + @override + void onInit() { + super.onInit(); + queryData(); + } + + @override + List? getDataList(List? response) { + if (response?.isNotEmpty == true) { + initialIndex = max(0, response!.indexWhere((e) => e.id == areaId)); + } + return response; + } + + @override + Future?>> customGetData() => + LiveHttp.liveRoomAreaList( + isLogin: isLogin, + parentid: parentAreaId, + ); +} diff --git a/lib/pages/live_area_detail/view.dart b/lib/pages/live_area_detail/view.dart new file mode 100644 index 000000000..6ea2ba9db --- /dev/null +++ b/lib/pages/live_area_detail/view.dart @@ -0,0 +1,94 @@ +import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/live/live_area_list/area_item.dart'; +import 'package:PiliPlus/pages/live_area_detail/child/view.dart'; +import 'package:PiliPlus/pages/live_area_detail/controller.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class LiveAreaDetailPage extends StatefulWidget { + const LiveAreaDetailPage({ + super.key, + required this.areaId, + required this.parentAreaId, + required this.parentName, + }); + + final dynamic areaId; + final dynamic parentAreaId; + final String parentName; + + @override + State createState() => _LiveAreaDetailPageState(); +} + +class _LiveAreaDetailPageState extends State { + late final _controller = Get.put( + LiveAreaDatailController(widget.areaId?.toString(), widget.parentAreaId)); + + @override + Widget build(BuildContext context) { + final ThemeData theme = Theme.of(context); + return Scaffold( + appBar: AppBar( + title: Text(widget.parentName), + actions: [ + IconButton( + onPressed: () { + // TODO: search + }, + icon: const Icon(Icons.search), + ), + const SizedBox(width: 16), + ], + ), + body: SafeArea( + top: false, + bottom: false, + child: Obx(() => _buildBody(theme, _controller.loadingState.value)), + ), + ); + } + + Widget _buildBody( + ThemeData theme, LoadingState?> loadingState) { + return switch (loadingState) { + Loading() => const SizedBox.shrink(), + Success() => loadingState.response?.isNotEmpty == true + ? DefaultTabController( + initialIndex: _controller.initialIndex, + length: loadingState.response!.length, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TabBar( + isScrollable: true, + tabAlignment: TabAlignment.start, + tabs: loadingState.response! + .map((e) => Tab(text: e.name ?? '')) + .toList(), + ), + Expanded( + child: tabBarView( + children: loadingState.response! + .map((e) => LiveAreaChildPage( + areaId: e.id, + parentAreaId: e.parentId, + )) + .toList(), + ), + ), + ], + ), + ) + : LiveAreaChildPage( + areaId: widget.areaId, + parentAreaId: widget.parentAreaId, + ), + Error() => LiveAreaChildPage( + areaId: widget.areaId, + parentAreaId: widget.parentAreaId, + ), + }; + } +} diff --git a/pubspec.lock b/pubspec.lock index ebf6039d9..8860532d0 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -757,6 +757,15 @@ packages: url: "https://pub.dev" source: hosted version: "4.9.8+7" + flutter_sortable_wrap: + dependency: "direct main" + description: + path: "." + ref: master + resolved-ref: ed2d47c7a8cc0d218bc322ab770fd19c0b56a863 + url: "https://github.com/bggRGjQaUbCoE/flutter_sortable_wrap.git" + source: git + version: "1.0.6" flutter_svg: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index c66c4ebc7..215152a85 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -192,6 +192,10 @@ dependencies: webdav_client: ^1.2.2 re_highlight: ^0.0.3 cached_network_svg_image: ^1.2.0 + flutter_sortable_wrap: + git: + url: https://github.com/bggRGjQaUbCoE/flutter_sortable_wrap.git + ref: master vector_math: any fixnum: any