bubble page

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-04-09 20:51:28 +08:00
parent db30aa8041
commit 222c9d01a0
22 changed files with 571 additions and 9 deletions

View File

@@ -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';
}

View File

@@ -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<LoadingState<BubbleData>> 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']);
}
}
}

View File

@@ -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<String, dynamic> json) => BaseInfo(
tribeInfo: json['tribee_info'] == null
? null
: TribeInfo.fromJson(json['tribee_info'] as Map<String, dynamic>),
isJoined: json['is_joined'] as bool?,
);
}

View File

@@ -0,0 +1,13 @@
class BasicInfo {
String? icon;
String? title;
String? jumpUri;
BasicInfo({this.icon, this.title, this.jumpUri});
factory BasicInfo.fromJson(Map<String, dynamic> json) => BasicInfo(
icon: json['icon'] as String?,
title: json['title'] as String?,
jumpUri: json['jump_uri'] as String?,
);
}

View File

@@ -0,0 +1,13 @@
import 'package:PiliPlus/models_new/bubble/category_list.dart';
class Category {
List<CategoryList>? categoryList;
Category({this.categoryList});
factory Category.fromJson(Map<String, dynamic> json) => Category(
categoryList: (json['category_list'] as List<dynamic>?)
?.map((e) => CategoryList.fromJson(e as Map<String, dynamic>))
.toList(),
);
}

View File

@@ -0,0 +1,13 @@
class CategoryList {
String? id;
String? name;
int? type;
CategoryList({this.id, this.name, this.type});
factory CategoryList.fromJson(Map<String, dynamic> json) => CategoryList(
id: json['id'] as String?,
name: json['name'] as String?,
type: json['type'] as int?,
);
}

View File

@@ -0,0 +1,15 @@
import 'package:PiliPlus/models_new/bubble/dyn_list.dart';
class Content {
String? count;
List<DynList>? dynList;
Content({this.count, this.dynList});
factory Content.fromJson(Map<String, dynamic> json) => Content(
count: json['count'] as String?,
dynList: (json['dyn_list'] as List<dynamic>?)
?.map((e) => DynList.fromJson(e as Map<String, dynamic>))
.toList(),
);
}

View File

@@ -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<String, dynamic> json) => BubbleData(
baseInfo: json['base_info'] == null
? null
: BaseInfo.fromJson(json['base_info'] as Map<String, dynamic>),
content: json['content'] == null
? null
: Content.fromJson(json['content'] as Map<String, dynamic>),
category: json['category'] == null
? null
: Category.fromJson(json['category'] as Map<String, dynamic>),
sortInfo: json['sort_info'] == null
? null
: SortInfo.fromJson(json['sort_info'] as Map<String, dynamic>),
);
}

View File

@@ -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<String, dynamic> json) => DynList(
dynId: json['dyn_id'] as String?,
title: json['title'] as String?,
meta: json['meta'] == null
? null
: Meta.fromJson(json['meta'] as Map<String, dynamic>),
);
}

View File

@@ -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<String, dynamic> 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?,
);
}

View File

@@ -0,0 +1,21 @@
import 'package:PiliPlus/models_new/bubble/sort_item.dart';
class SortInfo {
bool? showSort;
List<SortItem>? sortItems;
int? curSortType;
SortInfo({
this.showSort,
this.sortItems,
this.curSortType,
});
factory SortInfo.fromJson(Map<String, dynamic> json) => SortInfo(
showSort: json['show_sort'] as bool?,
sortItems: (json['sort_items'] as List<dynamic>?)
?.map((e) => SortItem.fromJson(e as Map<String, dynamic>))
.toList(),
curSortType: json['cur_sort_type'] as int?,
);
}

View File

@@ -0,0 +1,11 @@
class SortItem {
int? sortType;
String? text;
SortItem({this.sortType, this.text});
factory SortItem.fromJson(Map<String, dynamic> json) => SortItem(
sortType: json['sort_type'] as int?,
text: json['text'] as String?,
);
}

View File

@@ -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<String, dynamic> 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?,
);
}

View File

@@ -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<BubbleData, DynList>
with GetSingleTickerProviderStateMixin {
BubbleController(this.categoryId);
final Object? categoryId;
late final Object tribeId;
int? sortType;
final Rxn<SortInfo> sortInfo = Rxn<SortInfo>();
TabController? tabController;
final RxnString tribeName = RxnString();
final Rxn<List<CategoryList>> tabs = Rxn<List<CategoryList>>();
@override
void onInit() {
super.onInit();
tribeId = Get.arguments['id'];
queryData();
}
@override
List<DynList>? getDataList(BubbleData response) {
return response.content?.dynList;
}
@override
bool customHandleResponse(bool isRefresh, Success<BubbleData> 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<LoadingState<BubbleData>> 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();
}
}

244
lib/pages/bubble/view.dart Normal file
View File

@@ -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<BubblePage> createState() => _BubblePageState();
}
class _BubblePageState extends State<BubblePage>
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<BubbleController>(
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<List<DynList>?> 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;
}

View File

@@ -105,7 +105,8 @@ class _HistoryPageState extends State<HistoryPage>
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<HistoryPage>
},
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<HistoryPage>
CustomHorizontalDragGestureRecognizer.new,
children: [
KeepAliveWrapper(child: child),
..._historyController.tabs.map(
(item) => HistoryPage(type: item.type),
),
...tabs.map((item) => HistoryPage(type: item.type)),
],
),
),

View File

@@ -54,6 +54,7 @@ class _MemberGuardState extends State<MemberGuard> {
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: Text('$_userName的舰队${_count == null ? '' : '($_count)'}'),
),

View File

@@ -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: [

View File

@@ -45,6 +45,7 @@ class _MyReplyState extends State<MyReply> with DynMixin {
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: const Text('我的评论'),
actions: [

View File

@@ -143,7 +143,7 @@ class _PopularSeriesPageState extends State<PopularSeriesPage> 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<PopularSeriesPage> 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,
),

View File

@@ -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()),
];
}

View File

@@ -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) {