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

@@ -0,0 +1,101 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
class DisabledIcon<T extends Widget> 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<Icon> disable([double? lineLengthScale]) =>
DisabledIcon(lineLengthScale: lineLengthScale, child: this);
}

View File

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

View File

@@ -7,9 +7,9 @@ abstract class LoadingState<T> {
}
class Loading extends LoadingState<Never> {
Loading._internal();
const Loading._internal();
static final Loading _instance = Loading._internal();
static const Loading _instance = Loading._internal();
factory Loading() => _instance;
}

View File

@@ -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<String, dynamic> resultMap = json.decode(res.data);
if (resultMap['code'] == 0) {
return {
'status': true,
'data': HotSearchModel.fromJson(resultMap),
};
}
} else if (res.data is Map<String, dynamic> && 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<LoadingState<TrendingData>> 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<LoadingState<SearchKeywordData>> 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']);
}
}

View File

@@ -1,46 +0,0 @@
import 'package:hive/hive.dart';
part 'hot.g.dart';
@HiveType(typeId: 6)
class HotSearchModel {
HotSearchModel({
this.list,
});
@HiveField(0)
List<HotSearchItem>? list;
HotSearchModel.fromJson(Map<String, dynamic> json) {
list = (json['list'] as List?)
?.map<HotSearchItem>((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<String, dynamic> json) {
keyword = json['keyword'];
showName = json['show_name'];
wordType = json['word_type'];
icon = json['icon'];
}
}

View File

@@ -1,84 +0,0 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'hot.dart';
// **************************************************************************
// TypeAdapterGenerator
// **************************************************************************
class HotSearchModelAdapter extends TypeAdapter<HotSearchModel> {
@override
final int typeId = 6;
@override
HotSearchModel read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
};
return HotSearchModel(
list: (fields[0] as List?)?.cast<HotSearchItem>(),
);
}
@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<HotSearchItem> {
@override
final int typeId = 7;
@override
HotSearchItem read(BinaryReader reader) {
final numOfFields = reader.readByte();
final fields = <int, dynamic>{
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;
}

View File

@@ -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<String, dynamic> 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<String, dynamic>),
);
}
Map<String, dynamic> toJson() => {
'code': code,
'message': message,
'ttl': ttl,
'data': data?.toJson(),
};
}

View File

@@ -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<dynamic>? liveId;
int? callReason;
String? heatLayer;
int? pos;
int? id;
String? status;
String? nameType;
int? resourceId;
int? setGray;
List<dynamic>? 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<String, dynamic> 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<dynamic>?,
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<dynamic>?,
heatScore: json['heat_score'] as int?,
statDatas: json['stat_datas'] == null
? null
: StatDatas.fromJson(json['stat_datas'] as Map<String, dynamic>),
);
Map<String, dynamic> 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(),
};
}

View File

@@ -1,9 +1,19 @@
import 'package:PiliPlus/models/search/search_trending/trending_list.dart';
class TrendingData {
class SearchKeywordData {
List<SearchKeywordList>? list;
SearchKeywordData.fromJson(Map<String, dynamic> json) {
list =
(json['list'] as List?)?.map((e) => TrendingList.fromJson(e)).toList();
}
}
class TrendingData implements SearchKeywordData {
String? trackid;
List<TrendingList>? list;
List<TrendingList>? topList;
@override
List<SearchKeywordList>? list;
List<SearchKeywordList>? 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<String, dynamic> toJson() => {
'trackid': trackid,
'list': list?.map((e) => e.toJson()).toList(),
'top_list': topList?.map((e) => e.toJson()).toList(),
'hotword_egg_info': hotwordEggInfo,
};
}

View File

@@ -1,47 +1,28 @@
class TrendingList {
int? position;
class SearchKeywordList {
String? keyword;
String? showName;
int? wordType;
String? icon;
bool? showLiveIcon;
SearchKeywordList.fromJson(Map<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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<String, dynamic> 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?;
}
}

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}');
}
},
)
: 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: () {
isHot
? Row(
mainAxisSize: MainAxisSize.min,
children: [
text,
Padding(
padding: const EdgeInsets.only(left: 14),
child: SizedBox(
height: 34,
child: TextButton.icon(
onPressed: () {
Get.toNamed(
'/searchTrending',
parameters: {'tag': _tag},
);
},
child: Padding(
padding:
const EdgeInsets.symmetric(vertical: 6, horizontal: 10),
child: Text.rich(
label: Text(
'完整榜单',
style: TextStyle(
fontSize: 13,
color: Theme.of(context).colorScheme.outline,
color:
Theme.of(context).colorScheme.outline,
),
TextSpan(
children: [
TextSpan(
text: '完整榜单',
),
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
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: '搜索默认词',

View File

@@ -48,6 +48,8 @@ class AccountManager extends Interceptor {
Api.searchDefault,
Api.searchSuggest,
Api.liveList,
Api.searchTrending,
Api.searchRecommend,
},
AccountType.video: {Api.videoUrl, Api.bangumiVideoUrl}
};

View File

@@ -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',