diff --git a/lib/http/api.dart b/lib/http/api.dart index e98b163cc..7b6f64c42 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -1004,4 +1004,6 @@ abstract final class Api { static const String memberGuard = '${HttpString.liveBaseUrl}/xlive/app-ucenter/v1/guard/MainGuardCardAll'; + + static const String bubble = '/x/tribee/v1/dyn/all'; } diff --git a/lib/http/dynamics.dart b/lib/http/dynamics.dart index fac9b1aeb..7ef924a8d 100644 --- a/lib/http/dynamics.dart +++ b/lib/http/dynamics.dart @@ -15,6 +15,7 @@ import 'package:PiliPlus/models/dynamics/vote_model.dart'; import 'package:PiliPlus/models_new/article/article_info/data.dart'; import 'package:PiliPlus/models_new/article/article_list/data.dart'; import 'package:PiliPlus/models_new/article/article_view/data.dart'; +import 'package:PiliPlus/models_new/bubble/data.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_mention/data.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_mention/group.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_reserve/data.dart'; @@ -777,4 +778,30 @@ abstract final class DynamicsHttp { return Error(res.data['message']); } } + + static Future> bubble({ + required Object tribeId, + Object? categoryId, + int? sortType, + required int page, + }) async { + final res = await Request().get( + Api.bubble, + queryParameters: { + 'tribee_id': tribeId, + 'category_id': ?categoryId, + 'sort_type': ?sortType, + 'page_size': 20, + 'page_num': page, + 'web_location': 333.40165, + 'x-bili-device-req-json': + '{"platform":"web","device":"pc","spmid":"333.40165"}', + }, + ); + if (res.data['code'] == 0) { + return Success(BubbleData.fromJson(res.data['data'])); + } else { + return Error(res.data['message']); + } + } } diff --git a/lib/models_new/bubble/base_info.dart b/lib/models_new/bubble/base_info.dart new file mode 100644 index 000000000..66f5fae38 --- /dev/null +++ b/lib/models_new/bubble/base_info.dart @@ -0,0 +1,15 @@ +import 'package:PiliPlus/models_new/bubble/tribee_info.dart'; + +class BaseInfo { + TribeInfo? tribeInfo; + bool? isJoined; + + BaseInfo({this.tribeInfo, this.isJoined}); + + factory BaseInfo.fromJson(Map json) => BaseInfo( + tribeInfo: json['tribee_info'] == null + ? null + : TribeInfo.fromJson(json['tribee_info'] as Map), + isJoined: json['is_joined'] as bool?, + ); +} diff --git a/lib/models_new/bubble/basic_info.dart b/lib/models_new/bubble/basic_info.dart new file mode 100644 index 000000000..0e0282e08 --- /dev/null +++ b/lib/models_new/bubble/basic_info.dart @@ -0,0 +1,13 @@ +class BasicInfo { + String? icon; + String? title; + String? jumpUri; + + BasicInfo({this.icon, this.title, this.jumpUri}); + + factory BasicInfo.fromJson(Map json) => BasicInfo( + icon: json['icon'] as String?, + title: json['title'] as String?, + jumpUri: json['jump_uri'] as String?, + ); +} diff --git a/lib/models_new/bubble/category.dart b/lib/models_new/bubble/category.dart new file mode 100644 index 000000000..175182d0b --- /dev/null +++ b/lib/models_new/bubble/category.dart @@ -0,0 +1,13 @@ +import 'package:PiliPlus/models_new/bubble/category_list.dart'; + +class Category { + List? categoryList; + + Category({this.categoryList}); + + factory Category.fromJson(Map json) => Category( + categoryList: (json['category_list'] as List?) + ?.map((e) => CategoryList.fromJson(e as Map)) + .toList(), + ); +} diff --git a/lib/models_new/bubble/category_list.dart b/lib/models_new/bubble/category_list.dart new file mode 100644 index 000000000..dbff1b53c --- /dev/null +++ b/lib/models_new/bubble/category_list.dart @@ -0,0 +1,13 @@ +class CategoryList { + String? id; + String? name; + int? type; + + CategoryList({this.id, this.name, this.type}); + + factory CategoryList.fromJson(Map json) => CategoryList( + id: json['id'] as String?, + name: json['name'] as String?, + type: json['type'] as int?, + ); +} diff --git a/lib/models_new/bubble/content.dart b/lib/models_new/bubble/content.dart new file mode 100644 index 000000000..06ba8495b --- /dev/null +++ b/lib/models_new/bubble/content.dart @@ -0,0 +1,15 @@ +import 'package:PiliPlus/models_new/bubble/dyn_list.dart'; + +class Content { + String? count; + List? dynList; + + Content({this.count, this.dynList}); + + factory Content.fromJson(Map json) => Content( + count: json['count'] as String?, + dynList: (json['dyn_list'] as List?) + ?.map((e) => DynList.fromJson(e as Map)) + .toList(), + ); +} diff --git a/lib/models_new/bubble/data.dart b/lib/models_new/bubble/data.dart new file mode 100644 index 000000000..ea224cf4f --- /dev/null +++ b/lib/models_new/bubble/data.dart @@ -0,0 +1,33 @@ +import 'package:PiliPlus/models_new/bubble/base_info.dart'; +import 'package:PiliPlus/models_new/bubble/category.dart'; +import 'package:PiliPlus/models_new/bubble/content.dart'; +import 'package:PiliPlus/models_new/bubble/sort_info.dart'; + +class BubbleData { + BaseInfo? baseInfo; + Content? content; + Category? category; + SortInfo? sortInfo; + + BubbleData({ + this.baseInfo, + this.content, + this.category, + this.sortInfo, + }); + + factory BubbleData.fromJson(Map json) => BubbleData( + baseInfo: json['base_info'] == null + ? null + : BaseInfo.fromJson(json['base_info'] as Map), + content: json['content'] == null + ? null + : Content.fromJson(json['content'] as Map), + category: json['category'] == null + ? null + : Category.fromJson(json['category'] as Map), + sortInfo: json['sort_info'] == null + ? null + : SortInfo.fromJson(json['sort_info'] as Map), + ); +} diff --git a/lib/models_new/bubble/dyn_list.dart b/lib/models_new/bubble/dyn_list.dart new file mode 100644 index 000000000..373d0342a --- /dev/null +++ b/lib/models_new/bubble/dyn_list.dart @@ -0,0 +1,21 @@ +import 'package:PiliPlus/models_new/bubble/meta.dart'; + +class DynList { + String? dynId; + String? title; + Meta? meta; + + DynList({ + this.dynId, + this.title, + this.meta, + }); + + factory DynList.fromJson(Map json) => DynList( + dynId: json['dyn_id'] as String?, + title: json['title'] as String?, + meta: json['meta'] == null + ? null + : Meta.fromJson(json['meta'] as Map), + ); +} diff --git a/lib/models_new/bubble/meta.dart b/lib/models_new/bubble/meta.dart new file mode 100644 index 000000000..bf59b6888 --- /dev/null +++ b/lib/models_new/bubble/meta.dart @@ -0,0 +1,20 @@ +class Meta { + String? author; + String? timeText; + String? replyCount; + String? viewStat; + + Meta({ + this.author, + this.timeText, + this.replyCount, + this.viewStat, + }); + + factory Meta.fromJson(Map json) => Meta( + author: json['author'] as String?, + timeText: json['time_text'] as String?, + replyCount: json['reply_count'] as String?, + viewStat: json['view_stat'] as String?, + ); +} diff --git a/lib/models_new/bubble/sort_info.dart b/lib/models_new/bubble/sort_info.dart new file mode 100644 index 000000000..510de6021 --- /dev/null +++ b/lib/models_new/bubble/sort_info.dart @@ -0,0 +1,21 @@ +import 'package:PiliPlus/models_new/bubble/sort_item.dart'; + +class SortInfo { + bool? showSort; + List? sortItems; + int? curSortType; + + SortInfo({ + this.showSort, + this.sortItems, + this.curSortType, + }); + + factory SortInfo.fromJson(Map json) => SortInfo( + showSort: json['show_sort'] as bool?, + sortItems: (json['sort_items'] as List?) + ?.map((e) => SortItem.fromJson(e as Map)) + .toList(), + curSortType: json['cur_sort_type'] as int?, + ); +} diff --git a/lib/models_new/bubble/sort_item.dart b/lib/models_new/bubble/sort_item.dart new file mode 100644 index 000000000..0dbb2c73c --- /dev/null +++ b/lib/models_new/bubble/sort_item.dart @@ -0,0 +1,11 @@ +class SortItem { + int? sortType; + String? text; + + SortItem({this.sortType, this.text}); + + factory SortItem.fromJson(Map json) => SortItem( + sortType: json['sort_type'] as int?, + text: json['text'] as String?, + ); +} diff --git a/lib/models_new/bubble/tribee_info.dart b/lib/models_new/bubble/tribee_info.dart new file mode 100644 index 000000000..6f2074e9b --- /dev/null +++ b/lib/models_new/bubble/tribee_info.dart @@ -0,0 +1,26 @@ +class TribeInfo { + String? id; + String? title; + String? subTitle; + String? faceUrl; + String? jumpUri; + String? summary; + + TribeInfo({ + this.id, + this.title, + this.subTitle, + this.faceUrl, + this.jumpUri, + this.summary, + }); + + factory TribeInfo.fromJson(Map json) => TribeInfo( + id: json['id'] as String?, + title: json['title'] as String?, + subTitle: json['sub_title'] as String?, + faceUrl: json['face_url'] as String?, + jumpUri: json['jump_uri'] as String?, + summary: json['summary'] as String?, + ); +} diff --git a/lib/pages/bubble/controller.dart b/lib/pages/bubble/controller.dart new file mode 100644 index 000000000..c39a51b87 --- /dev/null +++ b/lib/pages/bubble/controller.dart @@ -0,0 +1,77 @@ +import 'package:PiliPlus/http/dynamics.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models_new/bubble/category_list.dart'; +import 'package:PiliPlus/models_new/bubble/data.dart'; +import 'package:PiliPlus/models_new/bubble/dyn_list.dart'; +import 'package:PiliPlus/models_new/bubble/sort_info.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; +import 'package:flutter/material.dart' show TabController; +import 'package:get/get.dart'; + +class BubbleController extends CommonListController + with GetSingleTickerProviderStateMixin { + BubbleController(this.categoryId); + final Object? categoryId; + + late final Object tribeId; + int? sortType; + + final Rxn sortInfo = Rxn(); + TabController? tabController; + final RxnString tribeName = RxnString(); + final Rxn> tabs = Rxn>(); + + @override + void onInit() { + super.onInit(); + tribeId = Get.arguments['id']; + queryData(); + } + + @override + List? getDataList(BubbleData response) { + return response.content?.dynList; + } + + @override + bool customHandleResponse(bool isRefresh, Success response) { + if (isRefresh) { + final data = response.response; + sortInfo.value = data.sortInfo; + if (categoryId == null) { + tribeName.value = data.baseInfo?.tribeInfo?.title; + if (tabController == null) { + if (data.category?.categoryList case final categories? + when categories.isNotEmpty) { + tabController = TabController( + length: categories.length, + vsync: this, + ); + tabs.value = categories; + } + } + } + } + return false; + } + + @override + Future> customGetData() => DynamicsHttp.bubble( + tribeId: tribeId, + categoryId: categoryId, + sortType: sortType, + page: page, + ); + + @override + void onClose() { + tabController?.dispose(); + tabController = null; + super.onClose(); + } + + void onSort(int? sortType) { + this.sortType = sortType; + onReload(); + } +} diff --git a/lib/pages/bubble/view.dart b/lib/pages/bubble/view.dart new file mode 100644 index 000000000..159ece5ca --- /dev/null +++ b/lib/pages/bubble/view.dart @@ -0,0 +1,244 @@ +import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; +import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/http_error.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_new/bubble/dyn_list.dart'; +import 'package:PiliPlus/pages/bubble/controller.dart'; +import 'package:PiliPlus/utils/extension/scroll_controller_ext.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart' + hide ListTile, SliverGridDelegateWithMaxCrossAxisExtent; +import 'package:get/get.dart'; + +class BubblePage extends StatefulWidget { + const BubblePage({super.key, this.categoryId}); + + final String? categoryId; + + @override + State createState() => _BubblePageState(); +} + +class _BubblePageState extends State + with AutomaticKeepAliveClientMixin { + late final BubbleController _controller; + + @override + void initState() { + super.initState(); + _controller = Get.put( + BubbleController(widget.categoryId), + tag: widget.categoryId ?? 'all', + ); + } + + BubbleController currCtr([int? index]) { + try { + index ??= _controller.tabController!.index; + if (index != 0) { + return Get.find( + tag: _controller.tabs.value![index].id.toString(), + ); + } + } catch (_) {} + return _controller; + } + + @override + Widget build(BuildContext context) { + super.build(context); + final padding = MediaQuery.viewPaddingOf(context); + Widget child = refreshIndicator( + onRefresh: _controller.onRefresh, + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + controller: _controller.scrollController, + slivers: [ + SliverPadding( + padding: EdgeInsets.only(bottom: padding.bottom + 100), + sliver: Obx( + () => _buildBody(_controller.loadingState.value), + ), + ), + ], + ), + ); + if (widget.categoryId != null) { + return child; + } else { + child = Stack( + clipBehavior: .none, + children: [ + child, + Positioned( + right: kFloatingActionButtonMargin, + bottom: kFloatingActionButtonMargin + padding.bottom, + child: Obx( + () { + final sortInfo = _controller.sortInfo.value; + if (sortInfo == null || sortInfo.showSort != true) { + return const SizedBox.shrink(); + } + final item = sortInfo.sortItems?.firstWhereOrNull( + (e) => e.sortType == sortInfo.curSortType, + ); + if (item != null) { + return FloatingActionButton.extended( + tooltip: '排序', + onPressed: () => showDialog( + context: context, + builder: (context) => AlertDialog( + clipBehavior: .hardEdge, + contentPadding: const .symmetric(vertical: 12), + content: Column( + mainAxisSize: .min, + children: sortInfo.sortItems!.map( + (e) { + final isSelected = item.sortType == e.sortType; + return ListTile( + dense: true, + enabled: !isSelected, + onTap: () { + Get.back(); + if (!isSelected) { + _controller.onSort(e.sortType); + } + }, + title: Text( + e.text!, + style: const TextStyle(fontSize: 14), + ), + trailing: isSelected + ? const Icon(size: 22, Icons.check) + : null, + ); + }, + ).toList(), + ), + ), + ), + icon: const Icon(Icons.sort, size: 20), + label: Text(item.text!), + ); + } + return const SizedBox.shrink(); + }, + ), + ), + ], + ); + } + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar( + title: Obx(() { + final tribeName = _controller.tribeName.value; + if (tribeName == null) { + return const SizedBox.shrink(); + } + return Text('$tribeName小站'); + }), + ), + body: Padding( + padding: EdgeInsets.only(left: padding.left, right: padding.right), + child: Obx(() { + final tabs = _controller.tabs.value; + if (tabs == null || tabs.isEmpty) { + return child; + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TabBar( + isScrollable: true, + tabAlignment: .start, + controller: _controller.tabController, + onTap: (index) { + if (!_controller.tabController!.indexIsChanging) { + currCtr().scrollController.animToTop(); + } + }, + tabs: tabs.map((item) => Tab(text: item.name!)).toList(), + ), + Expanded( + child: tabBarView( + controller: _controller.tabController, + children: [ + KeepAliveWrapper(child: child), + ...tabs + .skip(1) + .map((item) => BubblePage(categoryId: item.id)), + ], + ), + ), + ], + ); + }), + ), + ); + } + + late final gridDelegate = SliverGridDelegateWithMaxCrossAxisExtent( + mainAxisExtent: 56, + maxCrossAxisExtent: 2 * Grid.smallCardWidth, + ); + + Widget _buildBody(LoadingState?> loadingState) { + switch (loadingState) { + case Loading(): + return const SliverFillRemaining(child: m3eLoading); + case Success(:final response): + if (response != null && response.isNotEmpty) { + return SliverGrid.builder( + gridDelegate: gridDelegate, + itemBuilder: (context, index) { + if (index == response.length - 1) { + _controller.onLoadMore(); + } + final item = response[index]; + return Material( + type: .transparency, + child: ListTile( + safeArea: false, + visualDensity: .standard, + // PageUtils.pushDynFromId(id: item.dynId); + onTap: () => Get.toNamed( + '/articlePage', + parameters: { + 'id': item.dynId!, + 'type': 'opus', + }, + ), + title: Text( + item.title!, + maxLines: 1, + overflow: .ellipsis, + ), + trailing: item.meta?.timeText == null + ? null + : Text( + item.meta!.timeText!, + style: const TextStyle(fontSize: 13), + ), + ), + ); + }, + itemCount: response.length, + ); + } + return HttpError(onReload: _controller.onReload); + case Error(:final errMsg): + return HttpError( + errMsg: errMsg, + onReload: _controller.onReload, + ); + } + } + + @override + bool get wantKeepAlive => widget.categoryId != null; +} diff --git a/lib/pages/history/view.dart b/lib/pages/history/view.dart index 8a4219c51..d8ba6540c 100644 --- a/lib/pages/history/view.dart +++ b/lib/pages/history/view.dart @@ -105,7 +105,8 @@ class _HistoryPageState extends State right: padding.right, ), child: Obx(() { - if (_historyController.tabs.isEmpty) { + final tabs = _historyController.tabs; + if (tabs.isEmpty) { return child; } return Column( @@ -128,9 +129,7 @@ class _HistoryPageState extends State }, tabs: [ const Tab(text: '全部'), - ..._historyController.tabs.map( - (item) => Tab(text: item.name), - ), + ...tabs.map((item) => Tab(text: item.name)), ], ), Expanded( @@ -143,9 +142,7 @@ class _HistoryPageState extends State CustomHorizontalDragGestureRecognizer.new, children: [ KeepAliveWrapper(child: child), - ..._historyController.tabs.map( - (item) => HistoryPage(type: item.type), - ), + ...tabs.map((item) => HistoryPage(type: item.type)), ], ), ), diff --git a/lib/pages/member_guard/view.dart b/lib/pages/member_guard/view.dart index 36855c8b2..f3c49db11 100644 --- a/lib/pages/member_guard/view.dart +++ b/lib/pages/member_guard/view.dart @@ -54,6 +54,7 @@ class _MemberGuardState extends State { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: false, appBar: AppBar( title: Text('$_userName的舰队${_count == null ? '' : '($_count)'}'), ), diff --git a/lib/pages/member_video_web/base/view.dart b/lib/pages/member_video_web/base/view.dart index 978ed474b..a476c194e 100644 --- a/lib/pages/member_video_web/base/view.dart +++ b/lib/pages/member_video_web/base/view.dart @@ -40,6 +40,7 @@ abstract class BaseVideoWebState< Widget build(BuildContext context) { final colorScheme = ColorScheme.of(context); return Scaffold( + resizeToAvoidBottomInset: false, appBar: AppBar( title: Text(name), actions: [ diff --git a/lib/pages/my_reply/view.dart b/lib/pages/my_reply/view.dart index 2f5412fad..a10553dc7 100644 --- a/lib/pages/my_reply/view.dart +++ b/lib/pages/my_reply/view.dart @@ -45,6 +45,7 @@ class _MyReplyState extends State with DynMixin { @override Widget build(BuildContext context) { return Scaffold( + resizeToAvoidBottomInset: false, appBar: AppBar( title: const Text('我的评论'), actions: [ diff --git a/lib/pages/popular_series/view.dart b/lib/pages/popular_series/view.dart index cb9b061ad..a36b5a13a 100644 --- a/lib/pages/popular_series/view.dart +++ b/lib/pages/popular_series/view.dart @@ -143,7 +143,7 @@ class _PopularSeriesPageState extends State with GridMixin { return ListTile( dense: true, minTileHeight: 44, - tileColor: isCurr ? Theme.of(context).highlightColor : null, + enabled: !isCurr, onTap: () { Get.back(); if (!isCurr) { @@ -156,7 +156,7 @@ class _PopularSeriesPageState extends State with GridMixin { item.name!, style: const TextStyle(fontSize: 14), ), - trailing: isCurr ? const Icon(Icons.check, size: 18) : null, + trailing: isCurr ? const Icon(Icons.check, size: 20) : null, contentPadding: const EdgeInsetsGeometry.symmetric( horizontal: 16, ), diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 9d3662d21..d26c3d522 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -3,6 +3,7 @@ import 'package:PiliPlus/pages/article/view.dart'; import 'package:PiliPlus/pages/article_list/view.dart'; import 'package:PiliPlus/pages/audio/view.dart'; import 'package:PiliPlus/pages/blacklist/view.dart'; +import 'package:PiliPlus/pages/bubble/view.dart'; import 'package:PiliPlus/pages/danmaku_block/view.dart'; import 'package:PiliPlus/pages/dlna/view.dart'; import 'package:PiliPlus/pages/download/view.dart'; @@ -198,5 +199,6 @@ class Routes { GetPage(name: '/videoWeb', page: () => const MemberVideoWeb()), GetPage(name: '/ssWeb', page: () => const MemberSSWeb()), GetPage(name: '/memberGuard', page: () => const MemberGuard()), + GetPage(name: '/bubble', page: () => const BubblePage()), ]; } diff --git a/lib/utils/app_scheme.dart b/lib/utils/app_scheme.dart index 678808488..8d9214895 100644 --- a/lib/utils/app_scheme.dart +++ b/lib/utils/app_scheme.dart @@ -809,6 +809,15 @@ abstract final class PiliScheme { } launchURL(); return false; + case 'bubble': + // https://www.bilibili.com/bubble/home/1 + final id = uriDigitRegExp.firstMatch(path)?.group(1); + if (id != null) { + Get.toNamed('/bubble', arguments: {'id': id}); + return true; + } + launchURL(); + return false; default: final res = IdUtils.matchAvorBv(input: area?.split('?').first); if (res.isNotEmpty) {