From f0e3b776bb82c4560a410767b5fd531d80d1edb9 Mon Sep 17 00:00:00 2001 From: My-Responsitories <107370289+My-Responsitories@users.noreply.github.com> Date: Wed, 16 Apr 2025 12:16:45 +0800 Subject: [PATCH] opt: unify trending api & feat: search recommend (#694) * opt: unify trending api * opt: disable icon * feat: search recommend * mod: recommend api --- lib/common/widgets/disabled_icon.dart | 101 ++++++++++++ lib/http/api.dart | 3 + lib/http/loading_state.dart | 4 +- lib/http/search.dart | 36 ++-- lib/models/search/hot.dart | 46 ------ lib/models/search/hot.g.dart | 84 ---------- .../search_trending/search_trending.dart | 28 ---- .../search/search_trending/top_list.dart | 95 ----------- .../search/search_trending/trending_data.dart | 23 +-- .../search/search_trending/trending_list.dart | 61 +++---- lib/pages/search/controller.dart | 33 ++-- lib/pages/search/view.dart | 156 ++++++++---------- lib/pages/search/widgets/hot_keyword.dart | 42 +++-- lib/pages/search_trending/controller.dart | 9 +- lib/pages/search_trending/view.dart | 2 +- lib/pages/setting/widgets/model.dart | 8 + .../accounts/account_manager/account_mgr.dart | 2 + lib/utils/storage.dart | 4 +- 18 files changed, 285 insertions(+), 452 deletions(-) create mode 100644 lib/common/widgets/disabled_icon.dart delete mode 100644 lib/models/search/hot.dart delete mode 100644 lib/models/search/hot.g.dart delete mode 100644 lib/models/search/search_trending/search_trending.dart delete mode 100644 lib/models/search/search_trending/top_list.dart diff --git a/lib/common/widgets/disabled_icon.dart b/lib/common/widgets/disabled_icon.dart new file mode 100644 index 00000000..61b65b3f --- /dev/null +++ b/lib/common/widgets/disabled_icon.dart @@ -0,0 +1,101 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +class DisabledIcon extends SingleChildRenderObjectWidget { + final Color? color; + final double lineLengthScale; + final StrokeCap strokeCap; + + const DisabledIcon({ + super.key, + required T child, + this.color, + double? lineLengthScale, + StrokeCap? strokeCap, + }) : lineLengthScale = lineLengthScale ?? 0.9, + strokeCap = strokeCap ?? StrokeCap.butt, + super(child: child); + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderMaskedIcon( + color ?? + (child is Icon + ? (child as Icon).color ?? IconTheme.of(context).color! + : IconTheme.of(context).color!), + lineLengthScale, + strokeCap, + ); + } + + T enable() => child as T; +} + +class RenderMaskedIcon extends RenderProxyBox { + final Color color; + final double lineLengthScale; + final StrokeCap strokeCap; + + RenderMaskedIcon(this.color, this.lineLengthScale, this.strokeCap); + + @override + void paint(PaintingContext context, Offset offset) { + final strokeWidth = size.width / 12; + + final canvas = context.canvas; + var rect = offset & size; + + final sqrt2Width = strokeWidth * sqrt2; // rotate pi / 4 + + // final path = Path.combine( + // PathOperation.difference, + // Path()..addRect(rect), + // Path()..moveTo(rect.left, rect.top) + // ..relativeLineTo(sqrt2Width, 0) + // ..lineTo(rect.right, rect.bottom - sqrt2Width) + // ..lineTo(rect.right, rect.bottom) + // ..close(), + // ); + + final path = Path.combine( + PathOperation.union, + Path() // bottom + ..moveTo(rect.left, rect.bottom) + ..lineTo(rect.left, rect.top + sqrt2Width) + ..lineTo(rect.right - sqrt2Width, rect.bottom) + ..close(), + Path() // top + ..moveTo(rect.right, rect.top) + ..lineTo(rect.right, rect.bottom - sqrt2Width) + ..lineTo(rect.left + sqrt2Width, rect.top)); + + canvas.save(); + + canvas.clipPath(path, doAntiAlias: false); + super.paint(context, offset); + + context.canvas.restore(); + + final linePaint = Paint() + ..color = color + ..strokeWidth = strokeWidth + ..strokeCap = strokeCap; + + final strokeOffset = strokeWidth * sqrt1_2 / 2; + rect = rect + .translate(-strokeOffset, strokeOffset) + .deflate(size.width * lineLengthScale); + canvas.drawLine( + rect.topLeft, + rect.bottomRight, + linePaint, + ); + } +} + +extension DisabledIconExt on Icon { + DisabledIcon disable([double? lineLengthScale]) => + DisabledIcon(lineLengthScale: lineLengthScale, child: this); +} diff --git a/lib/http/api.dart b/lib/http/api.dart index b0ba10af..1d5fff3a 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -766,4 +766,7 @@ class Api { static const String searchTrending = '/x/v2/search/trending/ranking'; static const String setTopDyn = '/x/dynamic/feed/space/set_top'; + + static const String searchRecommend = + '${HttpString.appBaseUrl}/x/v2/search/recommend'; } diff --git a/lib/http/loading_state.dart b/lib/http/loading_state.dart index 229c5610..e8fb0953 100644 --- a/lib/http/loading_state.dart +++ b/lib/http/loading_state.dart @@ -7,9 +7,9 @@ abstract class LoadingState { } class Loading extends LoadingState { - Loading._internal(); + const Loading._internal(); - static final Loading _instance = Loading._internal(); + static const Loading _instance = Loading._internal(); factory Loading() => _instance; } diff --git a/lib/http/search.dart b/lib/http/search.dart index 41dc391d..d9467b08 100644 --- a/lib/http/search.dart +++ b/lib/http/search.dart @@ -6,33 +6,12 @@ import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:PiliPlus/http/loading_state.dart'; import '../models/bangumi/info.dart'; import '../models/common/search_type.dart'; -import '../models/search/hot.dart'; import '../models/search/result.dart'; import '../models/search/suggest.dart'; import '../utils/storage.dart'; import 'index.dart'; class SearchHttp { - static Future hotSearchList() async { - var res = await Request().get(Api.hotSearchList); - if (res.data is String) { - Map resultMap = json.decode(res.data); - if (resultMap['code'] == 0) { - return { - 'status': true, - 'data': HotSearchModel.fromJson(resultMap), - }; - } - } else if (res.data is Map && res.data['code'] == 0) { - return { - 'status': true, - 'data': HotSearchModel.fromJson(res.data), - }; - } - - return {'status': false, 'msg': '请求错误'}; - } - // 获取搜索建议 static Future searchSuggest({required term}) async { var res = await Request().get(Api.searchSuggest, @@ -211,7 +190,7 @@ class SearchHttp { static Future> searchTrending( {int limit = 30}) async { - final dynamic res = await Request().get( + final res = await Request().get( Api.searchTrending, queryParameters: { 'limit': limit, @@ -223,4 +202,17 @@ class SearchHttp { return LoadingState.error(res.data['message']); } } + + static Future> searchRecommend() async { + final res = await Request().get(Api.searchRecommend, queryParameters: { + 'build': '8350200', + 'c_locale': 'zh_CN', + 'mobi_app': 'android', + 'platform': 'android', + 's_locale': 'zh_CN', + }); + return res.data['code'] == 0 + ? LoadingState.success(SearchKeywordData.fromJson(res.data['data'])) + : LoadingState.error(res.data['message']); + } } diff --git a/lib/models/search/hot.dart b/lib/models/search/hot.dart deleted file mode 100644 index 97b4f9be..00000000 --- a/lib/models/search/hot.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:hive/hive.dart'; - -part 'hot.g.dart'; - -@HiveType(typeId: 6) -class HotSearchModel { - HotSearchModel({ - this.list, - }); - - @HiveField(0) - List? list; - - HotSearchModel.fromJson(Map json) { - list = (json['list'] as List?) - ?.map((e) => HotSearchItem.fromJson(e)) - .toList(); - } -} - -@HiveType(typeId: 7) -class HotSearchItem { - HotSearchItem({ - this.keyword, - this.showName, - this.wordType, - this.icon, - }); - - @HiveField(0) - String? keyword; - @HiveField(1) - String? showName; - // 4/5热 11话题 8普通 7直播 - @HiveField(2) - int? wordType; - @HiveField(3) - String? icon; - - HotSearchItem.fromJson(Map json) { - keyword = json['keyword']; - showName = json['show_name']; - wordType = json['word_type']; - icon = json['icon']; - } -} diff --git a/lib/models/search/hot.g.dart b/lib/models/search/hot.g.dart deleted file mode 100644 index a06dd475..00000000 --- a/lib/models/search/hot.g.dart +++ /dev/null @@ -1,84 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'hot.dart'; - -// ************************************************************************** -// TypeAdapterGenerator -// ************************************************************************** - -class HotSearchModelAdapter extends TypeAdapter { - @override - final int typeId = 6; - - @override - HotSearchModel read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return HotSearchModel( - list: (fields[0] as List?)?.cast(), - ); - } - - @override - void write(BinaryWriter writer, HotSearchModel obj) { - writer - ..writeByte(1) - ..writeByte(0) - ..write(obj.list); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is HotSearchModelAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} - -class HotSearchItemAdapter extends TypeAdapter { - @override - final int typeId = 7; - - @override - HotSearchItem read(BinaryReader reader) { - final numOfFields = reader.readByte(); - final fields = { - for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), - }; - return HotSearchItem( - keyword: fields[0] as String?, - showName: fields[1] as String?, - wordType: fields[2] as int?, - icon: fields[3] as String?, - ); - } - - @override - void write(BinaryWriter writer, HotSearchItem obj) { - writer - ..writeByte(4) - ..writeByte(0) - ..write(obj.keyword) - ..writeByte(1) - ..write(obj.showName) - ..writeByte(2) - ..write(obj.wordType) - ..writeByte(3) - ..write(obj.icon); - } - - @override - int get hashCode => typeId.hashCode; - - @override - bool operator ==(Object other) => - identical(this, other) || - other is HotSearchItemAdapter && - runtimeType == other.runtimeType && - typeId == other.typeId; -} diff --git a/lib/models/search/search_trending/search_trending.dart b/lib/models/search/search_trending/search_trending.dart deleted file mode 100644 index 6a2aad77..00000000 --- a/lib/models/search/search_trending/search_trending.dart +++ /dev/null @@ -1,28 +0,0 @@ -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/top_list.dart b/lib/models/search/search_trending/top_list.dart deleted file mode 100644 index 679c752f..00000000 --- a/lib/models/search/search_trending/top_list.dart +++ /dev/null @@ -1,95 +0,0 @@ -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 index d3fac84d..b392990d 100644 --- a/lib/models/search/search_trending/trending_data.dart +++ b/lib/models/search/search_trending/trending_data.dart @@ -1,9 +1,19 @@ import 'package:PiliPlus/models/search/search_trending/trending_list.dart'; -class TrendingData { +class SearchKeywordData { + List? list; + + SearchKeywordData.fromJson(Map json) { + list = + (json['list'] as List?)?.map((e) => TrendingList.fromJson(e)).toList(); + } +} + +class TrendingData implements SearchKeywordData { String? trackid; - List? list; - List? topList; + @override + List? list; + List? topList; String? hotwordEggInfo; TrendingData({this.trackid, this.list, this.topList, this.hotwordEggInfo}); @@ -18,11 +28,4 @@ class TrendingData { .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 index 7eabbb8a..5dd5d37a 100644 --- a/lib/models/search/search_trending/trending_list.dart +++ b/lib/models/search/search_trending/trending_list.dart @@ -1,47 +1,28 @@ -class TrendingList { - int? position; +class SearchKeywordList { String? keyword; - String? showName; - int? wordType; String? icon; + bool? showLiveIcon; + + SearchKeywordList.fromJson(Map json) { + keyword = json['keyword'] as String?; + } +} + +class TrendingList extends SearchKeywordList { + String? showName; + // 4/5热 11话题 8普通 7直播 + int? wordType; 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, - }; + TrendingList.fromJson(Map json) : super.fromJson(json) { + 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?; + } } diff --git a/lib/pages/search/controller.dart b/lib/pages/search/controller.dart index 09b80f2f..66f3fa55 100644 --- a/lib/pages/search/controller.dart +++ b/lib/pages/search/controller.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/widgets/dialog.dart'; import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/search/search_trending/trending_data.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:PiliPlus/http/search.dart'; @@ -8,9 +9,6 @@ import 'package:PiliPlus/utils/storage.dart'; import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart'; class SSearchController extends GetxController { - late final historyOff = - ''; - final searchFocusNode = FocusNode(); final controller = TextEditingController(); @@ -21,8 +19,13 @@ class SSearchController extends GetxController { String hintText = '搜索'; late bool enableHotKey; + late bool enableSearchRcmd; - Rx loadingState = LoadingState.loading().obs; + Rx> loadingState = + LoadingState.loading().obs; + + Rx> recommendData = + LoadingState.loading().obs; int initIndex = 0; @@ -32,6 +35,8 @@ class SSearchController extends GetxController { late final searchSuggestion = GStorage.searchSuggestion; late final RxBool recordSearchHistory = GStorage.recordSearchHistory.obs; + late final digitOnlyRegExp = RegExp(r'^\d+$'); + @override void onInit() { super.onInit(); @@ -53,13 +58,20 @@ class SSearchController extends GetxController { enableHotKey = GStorage.setting.get(SettingBoxKey.enableHotKey, defaultValue: true); + enableSearchRcmd = GStorage.setting + .get(SettingBoxKey.enableSearchRcmd, defaultValue: true); + if (enableHotKey) { queryHotSearchList(); } + + if (enableSearchRcmd) { + queryRecommendList(); + } } void validateUid() { - showUidBtn.value = RegExp(r'^\d+$').hasMatch(controller.text); + showUidBtn.value = digitOnlyRegExp.hasMatch(controller.text); } void onChange(String value) { @@ -112,12 +124,11 @@ class SSearchController extends GetxController { // 获取热搜关键词 Future queryHotSearchList() async { - dynamic result = await SearchHttp.hotSearchList(); - if (result['status']) { - loadingState.value = LoadingState.success(result['data'].list); - } else { - loadingState.value = LoadingState.error(result['msg']); - } + loadingState.value = await SearchHttp.searchTrending(limit: 10); + } + + Future queryRecommendList() async { + recommendData.value = await SearchHttp.searchRecommend(); } void onClickKeyword(String keyword) { diff --git a/lib/pages/search/view.dart b/lib/pages/search/view.dart index fe1cda12..f74aee74 100644 --- a/lib/pages/search/view.dart +++ b/lib/pages/search/view.dart @@ -1,10 +1,11 @@ +import 'package:PiliPlus/common/widgets/disabled_icon.dart'; import 'package:PiliPlus/common/widgets/loading_widget.dart'; import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/search/search_trending/trending_data.dart'; import 'package:PiliPlus/models/search/suggest.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:get/get.dart'; import 'controller.dart'; import 'widgets/hot_keyword.dart'; @@ -42,11 +43,8 @@ class _SearchPageState extends State { tooltip: 'UID搜索用户', icon: const Icon(Icons.person_outline, size: 22), onPressed: () { - if (RegExp(r'^\d+$') - .hasMatch(_searchController.controller.text)) { - Get.toNamed( - '/member?mid=${_searchController.controller.text}'); - } + Get.toNamed( + '/member?mid=${_searchController.controller.text}'); }, ) : const SizedBox.shrink(), @@ -83,11 +81,9 @@ class _SearchPageState extends State { // 搜索建议 if (_searchController.searchSuggestion) _searchSuggest(), if (context.orientation == Orientation.portrait) ...[ - if (_searchController.enableHotKey) - // 热搜 - hotSearch(), - // 搜索历史 - _history() + if (_searchController.enableHotKey) hotSearch(), + _history(), + if (_searchController.enableSearchRcmd) hotSearch(false) ] else Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -95,6 +91,8 @@ class _SearchPageState extends State { if (_searchController.enableHotKey) Expanded(child: hotSearch()), Expanded(child: _history()), + if (_searchController.enableSearchRcmd) + Expanded(child: hotSearch(false)), ], ), ], @@ -135,7 +133,15 @@ class _SearchPageState extends State { ); } - Widget hotSearch() { + Widget hotSearch([bool isHot = true]) { + final text = Text( + isHot ? '大家都在搜' : '搜索发现', + strutStyle: const StrutStyle(leading: 0, height: 1), + style: Theme.of(context) + .textTheme + .titleMedium! + .copyWith(height: 1, fontWeight: FontWeight.bold), + ); return Padding( padding: const EdgeInsets.fromLTRB(10, 25, 4, 25), child: Column( @@ -144,61 +150,54 @@ class _SearchPageState extends State { Padding( padding: const EdgeInsets.fromLTRB(6, 0, 6, 6), child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text( - '大家都在搜', - strutStyle: StrutStyle(leading: 0, height: 1), - style: Theme.of(context) - .textTheme - .titleMedium! - .copyWith(height: 1, fontWeight: FontWeight.bold), - ), - const SizedBox(width: 12), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - Get.toNamed( - '/searchTrending', - parameters: {'tag': _tag}, - ); - }, - child: Padding( - padding: - const EdgeInsets.symmetric(vertical: 6, horizontal: 10), - child: Text.rich( - style: TextStyle( - fontSize: 13, - color: Theme.of(context).colorScheme.outline, - ), - TextSpan( + isHot + ? Row( + mainAxisSize: MainAxisSize.min, children: [ - TextSpan( - text: '完整榜单', - ), - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Icon( - size: 16, - Icons.keyboard_arrow_right, - color: Theme.of(context).colorScheme.outline, + text, + Padding( + padding: const EdgeInsets.only(left: 14), + child: SizedBox( + height: 34, + child: TextButton.icon( + onPressed: () { + Get.toNamed( + '/searchTrending', + parameters: {'tag': _tag}, + ); + }, + label: Text( + '完整榜单', + style: TextStyle( + fontSize: 13, + color: + Theme.of(context).colorScheme.outline, + ), + ), + icon: Icon( + size: 16, + Icons.keyboard_arrow_right, + color: Theme.of(context).colorScheme.outline, + ), + iconAlignment: IconAlignment.end, + ), ), - ), + ) ], - ), - ), - ), - ), - const Spacer(), + ) + : text, SizedBox( height: 34, child: TextButton.icon( style: ButtonStyle( - padding: WidgetStateProperty.all( - const EdgeInsets.only( - left: 10, top: 6, bottom: 6, right: 10), - ), + padding: const WidgetStatePropertyAll( + EdgeInsets.symmetric(horizontal: 10, vertical: 6)), ), - onPressed: _searchController.queryHotSearchList, + onPressed: isHot + ? _searchController.queryHotSearchList + : _searchController.queryRecommendList, icon: Icon( Icons.refresh_outlined, size: 18, @@ -215,7 +214,11 @@ class _SearchPageState extends State { ], ), ), - Obx(() => _buildHotKey(_searchController.loadingState.value)), + Obx(() => _buildHotKey( + isHot + ? _searchController.loadingState.value + : _searchController.recommendData.value, + )), ], ), ); @@ -223,8 +226,7 @@ class _SearchPageState extends State { Widget _history() { return Obx( - () => Container( - width: double.infinity, + () => Padding( padding: EdgeInsets.fromLTRB( 10, context.orientation == Orientation.landscape @@ -233,7 +235,7 @@ class _SearchPageState extends State { ? 0 : 6, 6, - MediaQuery.of(context).padding.bottom + 50, + 25, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -262,25 +264,8 @@ class _SearchPageState extends State { ? '记录搜索' : '无痕搜索', icon: _searchController.recordSearchHistory.value - ? Icon( - Icons.history, - color: Theme.of(context) - .colorScheme - .onSurfaceVariant - .withOpacity(0.8), - ) - : SvgPicture.string( - width: 22, - height: 22, - colorFilter: ColorFilter.mode( - Theme.of(context) - .colorScheme - .outline - .withOpacity(0.8), - BlendMode.srcIn, - ), - _searchController.historyOff, - ), + ? historyIcon + : historyIcon.disable(), style: IconButton.styleFrom( padding: EdgeInsets.zero, ), @@ -349,13 +334,16 @@ class _SearchPageState extends State { ); } - Widget _buildHotKey(LoadingState loadingState) { + Icon get historyIcon => Icon(Icons.history, + color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.8)); + + Widget _buildHotKey(LoadingState loadingState) { return switch (loadingState) { - Success() => (loadingState.response as List?)?.isNotEmpty == true + Success() => loadingState.response.list?.isNotEmpty == true ? LayoutBuilder( builder: (context, constraints) => HotKeyword( width: constraints.maxWidth, - hotSearchList: loadingState.response, + hotSearchList: loadingState.response.list!, onClick: _searchController.onClickKeyword, ), ) diff --git a/lib/pages/search/widgets/hot_keyword.dart b/lib/pages/search/widgets/hot_keyword.dart index e38d70fa..16086693 100644 --- a/lib/pages/search/widgets/hot_keyword.dart +++ b/lib/pages/search/widgets/hot_keyword.dart @@ -1,17 +1,20 @@ +import 'package:PiliPlus/models/search/search_trending/trending_list.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; class HotKeyword extends StatelessWidget { - final double? width; - final List? hotSearchList; + final double width; + final List hotSearchList; final Function? onClick; + final bool showMore; const HotKeyword({ - this.width, - this.hotSearchList, - this.onClick, super.key, - }); + required double width, + required this.hotSearchList, + this.onClick, + this.showMore = true, + }) : this.width = width / 2 - 4; @override Widget build(BuildContext context) { @@ -19,22 +22,19 @@ class HotKeyword extends StatelessWidget { runSpacing: 0.4, spacing: 5.0, children: [ - for (var i in hotSearchList!) + for (var i in hotSearchList) SizedBox( - width: width! / 2 - 4, + width: width, child: Material( color: Colors.transparent, - borderRadius: BorderRadius.circular(3), + borderRadius: const BorderRadius.all(Radius.circular(3)), clipBehavior: Clip.hardEdge, child: InkWell( onTap: () => onClick?.call(i.keyword), child: Padding( - padding: EdgeInsets.only( - left: 2, - right: 10, - ), + padding: const EdgeInsets.only(left: 2, right: 10), child: Tooltip( - message: i.keyword!, + message: i.keyword, child: Row( children: [ Flexible( @@ -48,16 +48,14 @@ class HotKeyword extends StatelessWidget { ), ), ), - if (i.icon != null && i.icon != '') ...[ - const SizedBox(width: 4), - SizedBox( - height: 15, + if (!i.icon.isNullOrEmpty) + Padding( + padding: const EdgeInsets.only(left: 4), child: CachedNetworkImage( - imageUrl: (i.icon as String).http2https, - height: 15.0, + imageUrl: i.icon!.http2https, + height: 15, ), - ), - ] + ) ], ), ), diff --git a/lib/pages/search_trending/controller.dart b/lib/pages/search_trending/controller.dart index 2fd8c246..6a19ee79 100644 --- a/lib/pages/search_trending/controller.dart +++ b/lib/pages/search_trending/controller.dart @@ -5,7 +5,7 @@ import 'package:PiliPlus/models/search/search_trending/trending_list.dart'; import 'package:PiliPlus/pages/common/common_list_controller.dart'; class SearchTrendingController - extends CommonListController { + extends CommonListController { int topCount = 0; @override @@ -15,10 +15,11 @@ class SearchTrendingController } @override - List? getDataList(TrendingData response) { - List topList = (response.topList ?? []); + List? getDataList(TrendingData response) { + List topList = response.topList ?? []; topCount = topList.length; - return topList + (response.list ?? []); + return response.list == null ? topList : topList + ..addAll(response.list ?? []); } @override diff --git a/lib/pages/search_trending/view.dart b/lib/pages/search_trending/view.dart index 78f4a676..cea65f2e 100644 --- a/lib/pages/search_trending/view.dart +++ b/lib/pages/search_trending/view.dart @@ -123,7 +123,7 @@ class _SearchTrendingPageState extends State { ); } - Widget _buildBody(LoadingState?> loadingState) { + Widget _buildBody(LoadingState?> loadingState) { return switch (loadingState) { Loading() => SliverToBoxAdapter(child: LinearProgressIndicator()), Success() => loadingState.response?.isNotEmpty == true diff --git a/lib/pages/setting/widgets/model.dart b/lib/pages/setting/widgets/model.dart index dd17e91b..3d3a5f88 100644 --- a/lib/pages/setting/widgets/model.dart +++ b/lib/pages/setting/widgets/model.dart @@ -2212,6 +2212,14 @@ List get extraSettings => [ setKey: SettingBoxKey.enableHotKey, defaultVal: true, ), + SettingsModel( + settingsType: SettingsType.sw1tch, + title: '搜索发现', + subtitle: '是否展示「搜索发现」', + leading: const Icon(Icons.search_outlined), + setKey: SettingBoxKey.enableSearchRcmd, + defaultVal: true, + ), SettingsModel( settingsType: SettingsType.sw1tch, title: '搜索默认词', diff --git a/lib/utils/accounts/account_manager/account_mgr.dart b/lib/utils/accounts/account_manager/account_mgr.dart index 06e78d4e..4ba7061b 100644 --- a/lib/utils/accounts/account_manager/account_mgr.dart +++ b/lib/utils/accounts/account_manager/account_mgr.dart @@ -48,6 +48,8 @@ class AccountManager extends Interceptor { Api.searchDefault, Api.searchSuggest, Api.liveList, + Api.searchTrending, + Api.searchRecommend, }, AccountType.video: {Api.videoUrl, Api.bangumiVideoUrl} }; diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 81347c8b..bc563622 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -33,7 +33,6 @@ import 'package:get/get_navigation/src/routes/transitions_type.dart'; import 'package:hive_flutter/hive_flutter.dart'; import 'package:path_provider/path_provider.dart'; import 'package:PiliPlus/models/model_owner.dart'; -import 'package:PiliPlus/models/search/hot.dart'; import 'package:PiliPlus/models/user/info.dart'; import 'global_data.dart'; import 'package:uuid/uuid.dart'; @@ -588,8 +587,6 @@ class GStorage { Hive.registerAdapter(OwnerAdapter()); Hive.registerAdapter(UserInfoDataAdapter()); Hive.registerAdapter(LevelInfoAdapter()); - Hive.registerAdapter(HotSearchModelAdapter()); - Hive.registerAdapter(HotSearchItemAdapter()); Hive.registerAdapter(BiliCookieJarAdapter()); Hive.registerAdapter(LoginAccountAdapter()); Hive.registerAdapter(AccountTypeAdapter()); @@ -685,6 +682,7 @@ class SettingBoxKey { replySortType = 'replySortType', defaultDynamicType = 'defaultDynamicType', enableHotKey = 'enableHotKey', + enableSearchRcmd = 'enableSearchRcmd', enableQuickFav = 'enableQuickFav', enableWordRe = 'enableWordRe', enableSearchWord = 'enableSearchWord',