mirror of
https://github.com/HChaZZY/PiliPlus.git
synced 2025-12-06 09:13:48 +08:00
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:
committed by
GitHub
parent
3638d65008
commit
f0e3b776bb
101
lib/common/widgets/disabled_icon.dart
Normal file
101
lib/common/widgets/disabled_icon.dart
Normal 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);
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: '搜索默认词',
|
||||
|
||||
@@ -48,6 +48,8 @@ class AccountManager extends Interceptor {
|
||||
Api.searchDefault,
|
||||
Api.searchSuggest,
|
||||
Api.liveList,
|
||||
Api.searchTrending,
|
||||
Api.searchRecommend,
|
||||
},
|
||||
AccountType.video: {Api.videoUrl, Api.bangumiVideoUrl}
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user