opt: unify trending api & feat: search recommend (#694)

* opt: unify trending api

* opt: disable icon

* feat: search recommend

* mod: recommend api
This commit is contained in:
My-Responsitories
2025-04-16 12:16:45 +08:00
committed by GitHub
parent 3638d65008
commit f0e3b776bb
18 changed files with 285 additions and 452 deletions

View File

@@ -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 =
'<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#000000"><path d="m785-289-58-58q16-29 24.5-63t8.5-70q0-117-81.5-198.5T480-760q-35 0-68.5 8.5T348-726l-59-59q43-26 91.5-40.5T480-840q75 0 140.5 28.5t114 77q48.5 48.5 77 114T840-480q0 53-14.5 101T785-289ZM520-554l-80-80v-46h80v126ZM792-56 672-176q-42 26-90 41t-102 15q-138 0-240.5-91.5T122-440h82q14 104 92.5 172T480-200q37 0 70.5-8.5T614-234L288-560H120v-168l-64-64 56-56 736 736-56 56Z"/></svg>';
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 = LoadingState.loading().obs;
Rx<LoadingState<TrendingData>> loadingState =
LoadingState<TrendingData>.loading().obs;
Rx<LoadingState<SearchKeywordData>> recommendData =
LoadingState<SearchKeywordData>.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) {

View File

@@ -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<SearchPage> {
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<SearchPage> {
// 搜索建议
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<SearchPage> {
if (_searchController.enableHotKey)
Expanded(child: hotSearch()),
Expanded(child: _history()),
if (_searchController.enableSearchRcmd)
Expanded(child: hotSearch(false)),
],
),
],
@@ -135,7 +133,15 @@ class _SearchPageState extends State<SearchPage> {
);
}
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<SearchPage> {
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<SearchPage> {
],
),
),
Obx(() => _buildHotKey(_searchController.loadingState.value)),
Obx(() => _buildHotKey(
isHot
? _searchController.loadingState.value
: _searchController.recommendData.value,
)),
],
),
);
@@ -223,8 +226,7 @@ class _SearchPageState extends State<SearchPage> {
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<SearchPage> {
? 0
: 6,
6,
MediaQuery.of(context).padding.bottom + 50,
25,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -262,25 +264,8 @@ class _SearchPageState extends State<SearchPage> {
? '记录搜索'
: '无痕搜索',
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<SearchPage> {
);
}
Widget _buildHotKey(LoadingState loadingState) {
Icon get historyIcon => Icon(Icons.history,
color: Theme.of(context).colorScheme.onSurfaceVariant.withOpacity(0.8));
Widget _buildHotKey(LoadingState<SearchKeywordData> 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,
),
)

View File

@@ -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<SearchKeywordList> 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,
),
),
]
)
],
),
),

View File

@@ -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<TrendingData, TrendingList> {
extends CommonListController<TrendingData, SearchKeywordList> {
int topCount = 0;
@override
@@ -15,10 +15,11 @@ class SearchTrendingController
}
@override
List<TrendingList>? getDataList(TrendingData response) {
List<TrendingList> topList = (response.topList ?? <TrendingList>[]);
List<SearchKeywordList>? getDataList(TrendingData response) {
List<SearchKeywordList> topList = response.topList ?? <TrendingList>[];
topCount = topList.length;
return topList + (response.list ?? <TrendingList>[]);
return response.list == null ? topList : topList
..addAll(response.list ?? []);
}
@override

View File

@@ -123,7 +123,7 @@ class _SearchTrendingPageState extends State<SearchTrendingPage> {
);
}
Widget _buildBody(LoadingState<List<TrendingList>?> loadingState) {
Widget _buildBody(LoadingState<List<SearchKeywordList>?> loadingState) {
return switch (loadingState) {
Loading() => SliverToBoxAdapter(child: LinearProgressIndicator()),
Success() => loadingState.response?.isNotEmpty == true

View File

@@ -2212,6 +2212,14 @@ List<SettingsModel> 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: '搜索默认词',