diff --git a/assets/images/live/live_@28h.gif b/assets/images/live/live_@28h.gif new file mode 100644 index 000000000..86f5e588d Binary files /dev/null and b/assets/images/live/live_@28h.gif differ diff --git a/lib/http/api.dart b/lib/http/api.dart index 469b85dbe..d82ce44e3 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -762,4 +762,6 @@ class Api { '${HttpString.liveBaseUrl}/xlive/web-ucenter/v2/emoticon/GetEmoticons'; static const String pgcTimeline = '/pgc/web/timeline'; + + static const String searchTrending = '/x/v2/search/trending/ranking'; } diff --git a/lib/http/search.dart b/lib/http/search.dart index 8e7356ac1..41dc391d1 100644 --- a/lib/http/search.dart +++ b/lib/http/search.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'package:PiliPlus/models/search/search_trending/trending_data.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -207,4 +208,19 @@ class SearchHttp { return {'status': false, 'msg': res.data['message']}; } } + + static Future> searchTrending( + {int limit = 30}) async { + final dynamic res = await Request().get( + Api.searchTrending, + queryParameters: { + 'limit': limit, + }, + ); + if (res.data['code'] == 0) { + return LoadingState.success(TrendingData.fromJson(res.data['data'])); + } else { + return LoadingState.error(res.data['message']); + } + } } diff --git a/lib/models/search/search_trending/search_trending.dart b/lib/models/search/search_trending/search_trending.dart new file mode 100644 index 000000000..6a2aad778 --- /dev/null +++ b/lib/models/search/search_trending/search_trending.dart @@ -0,0 +1,28 @@ +import 'trending_data.dart'; + +class SearchTrending { + int? code; + String? message; + int? ttl; + TrendingData? data; + + SearchTrending({this.code, this.message, this.ttl, this.data}); + + factory SearchTrending.fromJson(Map json) { + return SearchTrending( + code: json['code'] as int?, + message: json['message'] as String?, + ttl: json['ttl'] as int?, + data: json['data'] == null + ? null + : TrendingData.fromJson(json['data'] as Map), + ); + } + + Map toJson() => { + 'code': code, + 'message': message, + 'ttl': ttl, + 'data': data?.toJson(), + }; +} diff --git a/lib/models/search/search_trending/stat_datas.dart b/lib/models/search/search_trending/stat_datas.dart new file mode 100644 index 000000000..c77d32d46 --- /dev/null +++ b/lib/models/search/search_trending/stat_datas.dart @@ -0,0 +1,14 @@ + +class StatDatas { + StatDatas(); + + factory StatDatas.fromJson(Map json) { + // TODO: implement fromJson + throw UnimplementedError('StatDatas.fromJson($json) is not implemented'); + } + + Map toJson() { + // TODO: implement toJson + throw UnimplementedError(); + } +} \ No newline at end of file diff --git a/lib/models/search/search_trending/top_list.dart b/lib/models/search/search_trending/top_list.dart new file mode 100644 index 000000000..679c752f4 --- /dev/null +++ b/lib/models/search/search_trending/top_list.dart @@ -0,0 +1,95 @@ +import 'stat_datas.dart'; + +class TopList { + int? hotId; + String? keyword; + String? showName; + int? score; + int? wordType; + int? gotoType; + String? gotoValue; + String? icon; + List? liveId; + int? callReason; + String? heatLayer; + int? pos; + int? id; + String? status; + String? nameType; + int? resourceId; + int? setGray; + List? cardValues; + int? heatScore; + StatDatas? statDatas; + + TopList({ + this.hotId, + this.keyword, + this.showName, + this.score, + this.wordType, + this.gotoType, + this.gotoValue, + this.icon, + this.liveId, + this.callReason, + this.heatLayer, + this.pos, + this.id, + this.status, + this.nameType, + this.resourceId, + this.setGray, + this.cardValues, + this.heatScore, + this.statDatas, + }); + + factory TopList.fromJson(Map json) => TopList( + hotId: json['hot_id'] as int?, + keyword: json['keyword'] as String?, + showName: json['show_name'] as String?, + score: json['score'] as int?, + wordType: json['word_type'] as int?, + gotoType: json['goto_type'] as int?, + gotoValue: json['goto_value'] as String?, + icon: json['icon'] as String?, + liveId: json['live_id'] as List?, + callReason: json['call_reason'] as int?, + heatLayer: json['heat_layer'] as String?, + pos: json['pos'] as int?, + id: json['id'] as int?, + status: json['status'] as String?, + nameType: json['name_type'] as String?, + resourceId: json['resource_id'] as int?, + setGray: json['set_gray'] as int?, + cardValues: json['card_values'] as List?, + heatScore: json['heat_score'] as int?, + statDatas: json['stat_datas'] == null + ? null + : StatDatas.fromJson(json['stat_datas'] as Map), + ); + + Map toJson() => { + 'hot_id': hotId, + 'keyword': keyword, + 'show_name': showName, + 'score': score, + 'word_type': wordType, + 'goto_type': gotoType, + 'goto_value': gotoValue, + 'icon': icon, + 'live_id': liveId, + 'call_reason': callReason, + 'heat_layer': heatLayer, + 'pos': pos, + 'id': id, + 'status': status, + 'name_type': nameType, + 'resource_id': resourceId, + 'set_gray': setGray, + 'card_values': cardValues, + 'heat_score': heatScore, + 'stat_datas': statDatas?.toJson(), + }; +} diff --git a/lib/models/search/search_trending/trending_data.dart b/lib/models/search/search_trending/trending_data.dart new file mode 100644 index 000000000..d3fac84d6 --- /dev/null +++ b/lib/models/search/search_trending/trending_data.dart @@ -0,0 +1,28 @@ +import 'package:PiliPlus/models/search/search_trending/trending_list.dart'; + +class TrendingData { + String? trackid; + List? list; + List? topList; + String? hotwordEggInfo; + + TrendingData({this.trackid, this.list, this.topList, this.hotwordEggInfo}); + + factory TrendingData.fromJson(Map json) => TrendingData( + trackid: json['trackid'] as String?, + list: (json['list'] as List?) + ?.map((e) => TrendingList.fromJson(e as Map)) + .toList(), + topList: (json['top_list'] as List?) + ?.map((e) => TrendingList.fromJson(e as Map)) + .toList(), + hotwordEggInfo: json['hotword_egg_info'] as String?, + ); + + Map toJson() => { + 'trackid': trackid, + 'list': list?.map((e) => e.toJson()).toList(), + 'top_list': topList?.map((e) => e.toJson()).toList(), + 'hotword_egg_info': hotwordEggInfo, + }; +} diff --git a/lib/models/search/search_trending/trending_list.dart b/lib/models/search/search_trending/trending_list.dart new file mode 100644 index 000000000..7eabbb8a1 --- /dev/null +++ b/lib/models/search/search_trending/trending_list.dart @@ -0,0 +1,47 @@ +class TrendingList { + int? position; + String? keyword; + String? showName; + int? wordType; + String? icon; + int? hotId; + String? isCommercial; + int? resourceId; + bool? showLiveIcon; + + TrendingList({ + this.position, + this.keyword, + this.showName, + this.wordType, + this.icon, + this.hotId, + this.isCommercial, + this.resourceId, + this.showLiveIcon, + }); + + factory TrendingList.fromJson(Map json) => TrendingList( + position: json['position'] as int?, + keyword: json['keyword'] as String?, + showName: json['show_name'] as String?, + wordType: json['word_type'] as int?, + icon: json['icon'] as String?, + hotId: json['hot_id'] as int?, + isCommercial: json['is_commercial'] as String?, + resourceId: json['resource_id'] as int?, + showLiveIcon: json['show_live_icon'] as bool?, + ); + + Map toJson() => { + 'position': position, + 'keyword': keyword, + 'show_name': showName, + 'word_type': wordType, + 'icon': icon, + 'hot_id': hotId, + 'is_commercial': isCommercial, + 'resource_id': resourceId, + 'show_live_icon': showLiveIcon, + }; +} diff --git a/lib/pages/search/view.dart b/lib/pages/search/view.dart index 4a582ab20..a78798853 100644 --- a/lib/pages/search/view.dart +++ b/lib/pages/search/view.dart @@ -156,13 +156,7 @@ class _SearchPageState extends State with RouteAware { GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { - Get.toNamed( - '/webview', - parameters: { - 'url': - 'https://www.bilibili.com/blackboard/activity-trending-topic.html?navhide=1&native.theme=1&night=${Get.isDarkMode ? 1 : 0}' - }, - ); + Get.toNamed('/searchTrending'); }, child: Padding( padding: diff --git a/lib/pages/search_result/view.dart b/lib/pages/search_result/view.dart index 210852f32..ce5091ae0 100644 --- a/lib/pages/search_result/view.dart +++ b/lib/pages/search_result/view.dart @@ -64,7 +64,7 @@ class _SearchResultPageState extends State ), title: GestureDetector( onTap: () { - if (Get.previousRoute.startsWith('/search')) { + if (Get.previousRoute.startsWith('/search?')) { Get.back(); } else { Get.offNamed( diff --git a/lib/pages/search_trending/controller.dart b/lib/pages/search_trending/controller.dart new file mode 100644 index 000000000..2fd8c2466 --- /dev/null +++ b/lib/pages/search_trending/controller.dart @@ -0,0 +1,27 @@ +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/search.dart'; +import 'package:PiliPlus/models/search/search_trending/trending_data.dart'; +import 'package:PiliPlus/models/search/search_trending/trending_list.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; + +class SearchTrendingController + extends CommonListController { + int topCount = 0; + + @override + void onInit() { + super.onInit(); + queryData(); + } + + @override + List? getDataList(TrendingData response) { + List topList = (response.topList ?? []); + topCount = topList.length; + return topList + (response.list ?? []); + } + + @override + Future> customGetData() => + SearchHttp.searchTrending(); +} diff --git a/lib/pages/search_trending/view.dart b/lib/pages/search_trending/view.dart new file mode 100644 index 000000000..37541e092 --- /dev/null +++ b/lib/pages/search_trending/view.dart @@ -0,0 +1,207 @@ +import 'package:PiliPlus/common/widgets/http_error.dart'; +import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/search/search_trending/trending_list.dart'; +import 'package:PiliPlus/pages/search_trending/controller.dart'; +import 'package:PiliPlus/utils/extension.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; + +class SearchTrendingPage extends StatefulWidget { + const SearchTrendingPage({super.key}); + + @override + State createState() => _SearchTrendingPageState(); +} + +class _SearchTrendingPageState extends State { + final _controller = Get.put(SearchTrendingController()); + + late double _offset; + final RxDouble _scrollRatio = 0.0.obs; + + @override + void initState() { + super.initState(); + _controller.scrollController.addListener(listener); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _offset = Get.width * 528 / 1125 - 56 - Get.mediaQuery.padding.top; + } + + @override + void dispose() { + _controller.scrollController.removeListener(listener); + super.dispose(); + } + + void listener() { + _scrollRatio.value = clampDouble( + _controller.scrollController.position.pixels / _offset, + 0.0, + 1.0, + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + extendBody: true, + extendBodyBehindAppBar: true, + appBar: PreferredSize( + preferredSize: Size.fromHeight(56), + child: Obx( + () { + final half = _scrollRatio.value >= 0.5; + return AppBar( + title: Opacity( + opacity: _scrollRatio.value, + child: Text( + 'B站热搜', + style: TextStyle( + color: half ? null : Colors.white, + ), + ), + ), + backgroundColor: Theme.of(context) + .colorScheme + .surface + .withOpacity(_scrollRatio.value), + foregroundColor: half ? null : Colors.white, + systemOverlayStyle: half + ? null + : SystemUiOverlayStyle( + statusBarBrightness: Brightness.dark, + statusBarIconBrightness: Brightness.light, + ), + bottom: _scrollRatio.value == 1 + ? PreferredSize( + preferredSize: Size.fromHeight(1), + child: Divider( + height: 1, + color: Theme.of(context) + .colorScheme + .outline + .withOpacity(0.1), + ), + ) + : null, + ); + }, + ), + ), + body: refreshIndicator( + onRefresh: () async { + await _controller.onRefresh(); + }, + child: CustomScrollView( + controller: _controller.scrollController, + slivers: [ + SliverToBoxAdapter( + child: CachedNetworkImage( + fit: BoxFit.fitWidth, + imageUrl: + 'https://activity.hdslb.com/blackboard/activity59158/img/hot_banner.fbb081df.png', + ), + ), + Obx(() => _buildBody(_controller.loadingState.value)), + ], + ), + ), + ); + } + + Widget _buildBody(LoadingState?> loadingState) { + return switch (loadingState) { + Loading() => SliverToBoxAdapter(child: LinearProgressIndicator()), + Success() => loadingState.response?.isNotEmpty == true + ? SliverPadding( + padding: EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom + 100), + sliver: SliverList.separated( + itemCount: loadingState.response!.length, + itemBuilder: (context, index) { + final item = loadingState.response![index]; + return ListTile( + dense: true, + onTap: () { + Get.toNamed( + '/searchResult', + parameters: { + 'keyword': item.keyword!, + }, + ); + }, + leading: index < _controller.topCount + ? Icon( + size: 16, + Icons.vertical_align_top_outlined, + color: const Color(0xFFd1403e), + ) + : Text( + '${index + 1 - _controller.topCount}', + style: TextStyle( + fontWeight: FontWeight.bold, + color: switch (index - _controller.topCount) { + 0 => const Color(0xFFfdad13), + 1 => const Color(0xFF8aace1), + 2 => const Color(0xFFdfa777), + _ => Theme.of(context).colorScheme.outline, + }, + fontSize: 16, + fontStyle: FontStyle.italic, + ), + ), + title: Row( + children: [ + Flexible( + child: Text( + item.keyword!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + strutStyle: StrutStyle(height: 1, leading: 0), + style: TextStyle(height: 1, fontSize: 14), + ), + ), + if (item.icon?.isNotEmpty == true) ...[ + const SizedBox(width: 4), + CachedNetworkImage( + imageUrl: item.icon!.http2https, + height: 15, + ), + ] else if (item.showLiveIcon == true) ...[ + const SizedBox(width: 4), + Image.asset( + 'assets/images/live/live_@28h.gif', + width: 48, + height: 15, + ), + ], + ], + ), + ); + }, + separatorBuilder: (context, index) => Divider( + height: 1, + indent: 48, + color: Theme.of(context).colorScheme.outline.withOpacity(0.1), + ), + ), + ) + : HttpError( + callback: _controller.onReload, + ), + Error() => HttpError( + errMsg: loadingState.errMsg, + callback: _controller.onReload, + ), + _ => throw UnimplementedError(), + }; + } +} diff --git a/lib/pages/webview/webview_page.dart b/lib/pages/webview/webview_page.dart index 60064acbe..7f106b141 100644 --- a/lib/pages/webview/webview_page.dart +++ b/lib/pages/webview/webview_page.dart @@ -196,20 +196,6 @@ class _WebviewPageNewState extends State { } }, ); - controller.addJavaScriptHandler( - handlerName: "onSearch", - callback: (args) { - dynamic title = args.firstOrNull; - if (title != null) { - Get.toNamed( - '/searchResult', - parameters: { - 'keyword': title, - }, - ); - } - }, - ); }, onProgressChanged: (controller, progress) { this.progress.value = progress / 100; @@ -238,21 +224,6 @@ class _WebviewPageNewState extends State { document.styleSheets[0].insertRule('#app__display-area > div.control-panel {display:none;}', 0); ''', ); - } else if (url.startsWith( - 'https://www.bilibili.com/blackboard/activity-trending-topic.html')) { - controller.evaluateJavascript(source: ''' - document.addEventListener("click", function(e) { - const parentElement = e.target.parentElement; - if (parentElement) { - const trendingTitleElement = parentElement.querySelector(".trending-title"); - if (trendingTitleElement) { - const rankElement = trendingTitleElement.querySelector(".rank-index"); - const title = rankElement ? trendingTitleElement.innerText.replace(rankElement.innerText, "").trim() : trendingTitleElement.innerText; - window.flutter_inappwebview.callHandler("onSearch", title); - } - } - }); -'''); } // _webViewController?.evaluateJavascript( // source: ''' @@ -263,11 +234,6 @@ class _WebviewPageNewState extends State { }, onDownloadStartRequest: Platform.isAndroid ? (controller, request) { - if (_url.startsWith( - 'https://www.bilibili.com/blackboard/activity-trending-topic.html')) { - progress.value = 1; - return; - } showDialog( context: context, builder: (context) { @@ -340,9 +306,6 @@ class _WebviewPageNewState extends State { return NavigationActionPolicy.CANCEL; } else if (RegExp(r'^(?!(https?://))\S+://', caseSensitive: false) .hasMatch(url)) { - if (url.startsWith('bilibili://browser')) { - return NavigationActionPolicy.CANCEL; - } if (context.mounted) { SnackBar snackBar = SnackBar( content: const Text('当前网页将要打开外部链接,是否打开'), diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index ba2a1452d..b760faa59 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/pages/fav/view.dart'; import 'package:PiliPlus/pages/member/new/member_page.dart'; import 'package:PiliPlus/pages/member/new/widget/edit_profile_page.dart'; +import 'package:PiliPlus/pages/search_trending/view.dart'; import 'package:PiliPlus/pages/setting/navigation_bar_set.dart'; import 'package:PiliPlus/pages/setting/search_page.dart'; import 'package:PiliPlus/pages/setting/sponsor_block_page.dart'; @@ -178,6 +179,8 @@ class Routes { name: '/settingsSearch', page: () => const SettingsSearchPage()), CustomGetPage( name: '/webdavSetting', page: () => const WebDavSettingPage()), + CustomGetPage( + name: '/searchTrending', page: () => const SearchTrendingPage()), ]; }