opt trending header

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-06-30 19:23:41 +08:00
parent ed6353e6d5
commit f615a43259
5 changed files with 95 additions and 64 deletions

View File

@@ -0,0 +1,55 @@
import 'package:flutter/foundation.dart' show clampDouble;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart' show RenderSliverToBoxAdapter;
class TrendingHeader extends SliverToBoxAdapter {
const TrendingHeader({
super.key,
required this.offset,
required this.onScrollRatioChanged,
required super.child,
});
final double offset;
final ValueChanged<double> onScrollRatioChanged;
@override
RenderSliverToBoxAdapter createRenderObject(BuildContext context) {
return RenderTrendingHeader(
offset: offset,
onScrollRatioChanged: onScrollRatioChanged,
);
}
@override
void updateRenderObject(
BuildContext context,
RenderTrendingHeader renderObject,
) {
renderObject.offset = offset;
}
}
class RenderTrendingHeader extends RenderSliverToBoxAdapter {
RenderTrendingHeader({
required this.offset,
required this.onScrollRatioChanged,
});
double offset;
double? _scrollRatio;
final ValueChanged<double> onScrollRatioChanged;
@override
void performLayout() {
super.performLayout();
final scrollOffset = constraints.scrollOffset;
final scrollRatio = clampDouble(scrollOffset / offset, 0.0, 1.0);
if (_scrollRatio != scrollRatio) {
_scrollRatio = scrollRatio;
WidgetsBinding.instance.addPostFrameCallback((_) {
onScrollRatioChanged(scrollRatio);
});
}
}
}

View File

@@ -262,6 +262,7 @@ abstract final class SearchHttp {
static Future<LoadingState<SearchTrendingData>> searchTrending({ static Future<LoadingState<SearchTrendingData>> searchTrending({
int limit = 30, int limit = 30,
bool needsTop = false,
}) async { }) async {
final res = await Request().get( final res = await Request().get(
Api.searchTrending, Api.searchTrending,
@@ -270,7 +271,9 @@ abstract final class SearchHttp {
}, },
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return Success(SearchTrendingData.fromJson(res.data['data'])); return Success(
SearchTrendingData.fromJson(res.data['data'], needsTop: needsTop),
);
} else { } else {
return Error(res.data['message']); return Error(res.data['message']);
} }

View File

@@ -2,23 +2,27 @@ import 'package:PiliPlus/models_new/search/search_rcmd/data.dart';
import 'package:PiliPlus/models_new/search/search_trending/list.dart'; import 'package:PiliPlus/models_new/search/search_trending/list.dart';
class SearchTrendingData extends SearchRcmdData { class SearchTrendingData extends SearchRcmdData {
List<SearchTrendingItemModel>? topList; late int topCount;
SearchTrendingData({super.list, this.topList}); SearchTrendingData.fromJson(
Map<String, dynamic> json, {
factory SearchTrendingData.fromJson(Map<String, dynamic> json) => bool needsTop = false,
SearchTrendingData( }) {
list: (json['list'] as List<dynamic>?) list = (json['list'] as List<dynamic>?)
?.map( ?.map((e) => SearchTrendingItemModel.fromJson(e))
(e) => .toList();
SearchTrendingItemModel.fromJson(e as Map<String, dynamic>), if (needsTop) {
) final topList = (json['top_list'] as List<dynamic>?)
.toList(), ?.map((e) => SearchTrendingItemModel.fromJson(e))
topList: (json['top_list'] as List<dynamic>?) .toList();
?.map( topCount = topList?.length ?? 0;
(e) => if (topList != null && topList.isNotEmpty) {
SearchTrendingItemModel.fromJson(e as Map<String, dynamic>), if (list != null) {
) list!.insertAll(0, topList);
.toList(), } else {
); list = topList;
}
}
}
}
} }

View File

@@ -16,14 +16,11 @@ class SearchTrendingController
@override @override
List<SearchTrendingItemModel>? getDataList(SearchTrendingData response) { List<SearchTrendingItemModel>? getDataList(SearchTrendingData response) {
List<SearchTrendingItemModel> topList = topCount = response.topCount;
response.topList ?? <SearchTrendingItemModel>[]; return response.list;
topCount = topList.length;
return response.list == null ? topList : topList
..addAll(response.list ?? []);
} }
@override @override
Future<LoadingState<SearchTrendingData>> customGetData() => Future<LoadingState<SearchTrendingData>> customGetData() =>
SearchHttp.searchTrending(); SearchHttp.searchTrending(needsTop: true);
} }

View File

@@ -5,6 +5,7 @@ import 'package:PiliPlus/common/widgets/flutter/list_tile.dart';
import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
import 'package:PiliPlus/common/widgets/sliver/trending_header.dart';
import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models_new/search/search_trending/list.dart'; import 'package:PiliPlus/models_new/search/search_trending/list.dart';
import 'package:PiliPlus/pages/search_trending/controller.dart'; import 'package:PiliPlus/pages/search_trending/controller.dart';
@@ -15,7 +16,6 @@ import 'package:PiliPlus/utils/extension/num_ext.dart';
import 'package:PiliPlus/utils/extension/size_ext.dart'; import 'package:PiliPlus/utils/extension/size_ext.dart';
import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/image_utils.dart';
import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:cached_network_image_ce/cached_network_image.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide ListTile; import 'package:flutter/material.dart' hide ListTile;
import 'package:flutter/services.dart' show SystemUiOverlayStyle; import 'package:flutter/services.dart' show SystemUiOverlayStyle;
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -33,28 +33,6 @@ class _SearchTrendingPageState extends State<SearchTrendingPage> {
late double _offset; late double _offset;
final RxDouble _scrollRatio = 0.0.obs; final RxDouble _scrollRatio = 0.0.obs;
@override
void initState() {
super.initState();
_controller.scrollController.addListener(listener);
}
@override
void dispose() {
_controller.scrollController.removeListener(listener);
super.dispose();
}
void listener() {
if (_controller.scrollController.hasClients) {
_scrollRatio.value = clampDouble(
_controller.scrollController.position.pixels / _offset,
0.0,
1.0,
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
@@ -63,14 +41,13 @@ class _SearchTrendingPageState extends State<SearchTrendingPage> {
final maxWidth = size.width - padding.horizontal; final maxWidth = size.width - padding.horizontal;
final width = size.isPortrait ? maxWidth : min(640.0, maxWidth * 0.6); final width = size.isPortrait ? maxWidth : min(640.0, maxWidth * 0.6);
final height = width * 528 / 1125; final height = width * 528 / 1125;
_offset = height - 56 - padding.top; _offset = height - kToolbarHeight - padding.top;
listener();
return Scaffold( return Scaffold(
extendBody: true, extendBody: true,
extendBodyBehindAppBar: true, extendBodyBehindAppBar: true,
resizeToAvoidBottomInset: false, resizeToAvoidBottomInset: false,
appBar: PreferredSize( appBar: PreferredSize(
preferredSize: const Size.fromHeight(56), preferredSize: const Size.fromHeight(kToolbarHeight),
child: Obx( child: Obx(
() { () {
final scrollRatio = _scrollRatio.value; final scrollRatio = _scrollRatio.value;
@@ -80,9 +57,7 @@ class _SearchTrendingPageState extends State<SearchTrendingPage> {
opacity: scrollRatio, opacity: scrollRatio,
child: Text( child: Text(
'bilibili热搜', 'bilibili热搜',
style: TextStyle( style: TextStyle(color: flag ? null : Colors.white),
color: flag ? null : Colors.white,
),
), ),
), ),
backgroundColor: theme.colorScheme.surface.withValues( backgroundColor: theme.colorScheme.surface.withValues(
@@ -92,15 +67,13 @@ class _SearchTrendingPageState extends State<SearchTrendingPage> {
systemOverlayStyle: flag systemOverlayStyle: flag
? null ? null
: const SystemUiOverlayStyle( : const SystemUiOverlayStyle(
statusBarBrightness: Brightness.dark, statusBarBrightness: .dark,
statusBarIconBrightness: Brightness.light, statusBarIconBrightness: .light,
), ),
shape: scrollRatio == 1 shape: scrollRatio == 1
? Border( ? Border(
bottom: BorderSide( bottom: BorderSide(
color: theme.colorScheme.outline.withValues( color: theme.colorScheme.outline.withValues(alpha: 0.1),
alpha: 0.1,
),
), ),
) )
: null, : null,
@@ -109,10 +82,7 @@ class _SearchTrendingPageState extends State<SearchTrendingPage> {
), ),
), ),
body: Padding( body: Padding(
padding: EdgeInsets.only( padding: .only(left: padding.left, right: padding.right),
left: padding.left,
right: padding.right,
),
child: Center( child: Center(
child: SizedBox( child: SizedBox(
width: width, width: width,
@@ -122,7 +92,9 @@ class _SearchTrendingPageState extends State<SearchTrendingPage> {
controller: _controller.scrollController, controller: _controller.scrollController,
physics: const AlwaysScrollableScrollPhysics(), physics: const AlwaysScrollableScrollPhysics(),
slivers: [ slivers: [
SliverToBoxAdapter( TrendingHeader(
offset: _offset,
onScrollRatioChanged: _scrollRatio.call,
child: Image.asset( child: Image.asset(
width: width, width: width,
height: height, height: height,