feat: 热搜关键词

This commit is contained in:
guozhigq
2023-06-20 10:27:54 +08:00
parent 04704ae8f3
commit 335718b3a0
13 changed files with 483 additions and 8 deletions

View File

@ -140,4 +140,8 @@ class Api {
// 获取历史记录
static const String historyList = '/x/web-interface/history/cursor';
// 热搜
static const String hotSearchList =
'https://s.search.bilibili.com/main/hotword';
}

2
lib/http/index.dart Normal file
View File

@ -0,0 +1,2 @@
export 'api.dart';
export 'init.dart';

20
lib/http/search.dart Normal file
View File

@ -0,0 +1,20 @@
import 'package:pilipala/http/index.dart';
import 'package:pilipala/models/search/hot.dart';
class SearchHttp {
static Future hotSearchList() async {
var res = await Request().get(Api.hotSearchList);
if (res.data['code'] == 0) {
return {
'status': true,
'data': HotSearchModel.fromJson(res.data),
};
} else {
return {
'status': false,
'date': [],
'msg': '请求错误 🙅',
};
}
}
}

View File

@ -0,0 +1,46 @@
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']
.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

@ -0,0 +1,84 @@
// 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

@ -37,7 +37,9 @@ class HomeAppBar extends StatelessWidget {
),
actions: [
IconButton(
onPressed: () {},
onPressed: () {
Get.toNamed('/search');
},
icon: const Icon(CupertinoIcons.search, size: 22),
),
// IconButton(

View File

@ -0,0 +1,71 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:hive/hive.dart';
import 'package:pilipala/http/search.dart';
import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/utils/storage.dart';
class SearchController extends GetxController {
final FocusNode searchFocusNode = FocusNode();
RxString searchKeyWord = ''.obs;
Rx<TextEditingController> controller = TextEditingController().obs;
List tabs = [
{'label': '综合', 'id': ''},
{'label': '视频', 'id': ''},
{'label': '番剧', 'id': ''},
{'label': '直播', 'id': ''},
{'label': '专栏', 'id': ''},
{'label': '用户', 'id': ''}
];
List<HotSearchItem> hotSearchList = [];
Box hotKeyword = GStrorage.hotKeyword;
@override
void onInit() {
super.onInit();
if (hotKeyword.get('cacheList') != null &&
hotKeyword.get('cacheList').isNotEmpty) {
List<HotSearchItem> list = [];
for (var i in hotKeyword.get('cacheList')) {
list.add(i);
}
hotSearchList = list;
}
// 其他页面跳转过来
if (Get.parameters.keys.isNotEmpty) {
onClickKeyword(Get.parameters['keyword']!);
}
}
void onChange(value) {
searchKeyWord.value = value;
}
void onClear() {
controller.value.clear();
searchKeyWord.value = '';
}
void submit(value) {
searchKeyWord.value = value;
}
// 获取热搜关键词
Future queryHotSearchList() async {
var result = await SearchHttp.hotSearchList();
hotSearchList = result['data'].list;
hotKeyword.put('cacheList', result['data'].list);
return result;
}
// 点击热搜关键词
void onClickKeyword(String keyword) {
print(keyword);
searchKeyWord.value = keyword;
controller.value.text = keyword;
// 移动光标
controller.value.selection = TextSelection.fromPosition(
TextPosition(offset: controller.value.text.length),
);
}
}

View File

@ -0,0 +1,4 @@
library search;
export './controller.dart';
export './view.dart';

169
lib/pages/search/view.dart Normal file
View File

@ -0,0 +1,169 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:pilipala/common/widgets/http_error.dart';
import 'package:pilipala/pages/search/index.dart';
import 'widgets/hotKeyword.dart';
class SearchPage extends StatefulWidget {
const SearchPage({super.key});
@override
State<SearchPage> createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
final SearchController _searchController = Get.put(SearchController());
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
shape: Border(
bottom: BorderSide(
color: Theme.of(context).dividerColor.withOpacity(0.08),
width: 1,
),
),
titleSpacing: 0,
title: Obx(
() => TextField(
autofocus: true,
focusNode: _searchController.searchFocusNode,
controller: _searchController.controller.value,
textInputAction: TextInputAction.search,
onChanged: (value) => _searchController.onChange(value),
decoration: InputDecoration(
hintText: '搜索',
border: InputBorder.none,
suffixIcon: _searchController.searchKeyWord.value.isNotEmpty
? IconButton(
icon: Icon(
Icons.clear,
color: Theme.of(context).colorScheme.outline,
),
onPressed: () => _searchController.onClear())
: null,
),
onSubmitted: (String value) => _searchController.submit(value),
),
),
),
// body: Column(
// children: [hotSearch()],
// ),
body: hotSearch(),
// body: DefaultTabController(
// length: _searchController.tabs.length,
// child: Column(
// children: [
// const SizedBox(height: 4),
// Theme(
// data: ThemeData(
// splashColor: Colors.transparent, // 点击时的水波纹颜色设置为透明
// highlightColor: Colors.transparent, // 点击时的背景高亮颜色设置为透明
// ),
// child: TabBar(
// tabs: _searchController.tabs
// .map((e) => Tab(text: e['label']))
// .toList(),
// isScrollable: true,
// indicatorWeight: 0,
// indicatorPadding:
// const EdgeInsets.symmetric(horizontal: 3, vertical: 8),
// indicator: BoxDecoration(
// color: Theme.of(context).colorScheme.secondaryContainer,
// borderRadius: const BorderRadius.all(
// Radius.circular(16),
// ),
// ),
// indicatorSize: TabBarIndicatorSize.tab,
// labelColor: Theme.of(context).colorScheme.onSecondaryContainer,
// labelStyle: const TextStyle(fontSize: 13),
// dividerColor: Colors.transparent,
// unselectedLabelColor: Theme.of(context).colorScheme.outline,
// onTap: (index) {
// print(index);
// },
// ),
// ),
// Expanded(
// child: TabBarView(
// children: [
// Container(
// width: 200,
// height: 200,
// color: Colors.amber,
// ),
// Text('1'),
// Text('1'),
// Text('1'),
// Text('1'),
// Text('1'),
// ],
// ),
// ),
// ],
// ),
// ),
);
}
Widget hotSearch() {
return Padding(
padding: const EdgeInsets.fromLTRB(10, 25, 4, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(6, 0, 0, 0),
child: Text(
'大家都在搜',
style: Theme.of(context)
.textTheme
.titleMedium!
.copyWith(fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 6),
LayoutBuilder(
builder: (context, boxConstraints) {
final double width = boxConstraints.maxWidth;
return FutureBuilder(
future: _searchController.queryHotSearchList(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
Map data = snapshot.data as Map;
if (data['status']) {
return HotKeyword(
width: width,
hotSearchList: _searchController.hotSearchList,
onClick: (keyword) =>
_searchController.onClickKeyword(keyword),
);
} else {
return HttpError(
errMsg: data['msg'],
fn: () => setState(() {}),
);
}
} else {
// 缓存数据
if (_searchController.hotSearchList.isNotEmpty) {
return HotKeyword(
width: width,
hotSearchList: _searchController.hotSearchList,
);
} else {
return const SizedBox();
}
}
},
);
},
),
],
),
);
}
}

View File

@ -0,0 +1,60 @@
// ignore: file_names
import 'package:flutter/material.dart';
class HotKeyword extends StatelessWidget {
final double? width;
final List? hotSearchList;
final Function? onClick;
const HotKeyword({
this.width,
this.hotSearchList,
this.onClick,
super.key,
});
@override
Widget build(BuildContext context) {
return Wrap(
runSpacing: 0.4,
spacing: 5.0,
children: [
for (var i in hotSearchList!)
SizedBox(
width: width! / 2 - 8,
child: Material(
borderRadius: BorderRadius.circular(3),
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => onClick!(i.keyword),
child: Row(
children: [
SizedBox(
width: width! / 2 -
(i.icon != null && i.icon != '' ? 50 : 12),
child: Padding(
padding: const EdgeInsets.fromLTRB(6, 5, 0, 5),
child: Text(
i.keyword!,
overflow: TextOverflow.ellipsis,
maxLines: 1,
style: const TextStyle(fontSize: 14),
),
),
),
if (i.icon != null && i.icon != '')
SizedBox(
width: 40,
child: Image.network(
i.icon!,
height: 15,
),
),
],
),
),
),
),
],
);
}
}

View File

@ -243,9 +243,11 @@ class ReplyItem extends StatelessWidget {
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
),
child: Text('回复', style: Theme.of(context).textTheme.labelMedium!.copyWith(
color: Theme.of(context).colorScheme.outline
)),
child: Text('回复',
style: Theme.of(context)
.textTheme
.labelMedium!
.copyWith(color: Theme.of(context).colorScheme.outline)),
onPressed: () {
showModalBottomSheet(
context: context,
@ -504,10 +506,12 @@ InlineSpan buildContent(BuildContext context, content) {
style: TextStyle(
color: Theme.of(context).colorScheme.primary,
),
// recognizer: TapGestureRecognizer()
// ..onTap = () => {
// print('Url 点击'),
// },
recognizer: TapGestureRecognizer()
..onTap = () => {
Get.toNamed('/search', parameters: {
'keyword': content.jumpUrl[matchStr]['title']
})
},
),
);
spanChilds.add(

View File

@ -6,6 +6,7 @@ import 'package:pilipala/pages/home/index.dart';
import 'package:pilipala/pages/hot/index.dart';
import 'package:pilipala/pages/later/index.dart';
import 'package:pilipala/pages/preview/index.dart';
import 'package:pilipala/pages/search/index.dart';
import 'package:pilipala/pages/video/detail/index.dart';
import 'package:pilipala/pages/webview/index.dart';
import 'package:pilipala/pages/setting/index.dart';
@ -41,5 +42,7 @@ class Routes {
GetPage(name: '/later', page: () => const LaterPage()),
// 历史记录
GetPage(name: '/history', page: () => const HistoryPage()),
// 搜索页面
GetPage(name: '/search', page: () => const SearchPage())
];
}

View File

@ -3,12 +3,14 @@ import 'package:hive_flutter/hive_flutter.dart';
import 'package:path_provider/path_provider.dart';
import 'package:pilipala/models/model_owner.dart';
import 'package:pilipala/models/model_rec_video_item.dart';
import 'package:pilipala/models/search/hot.dart';
import 'package:pilipala/models/user/info.dart';
class GStrorage {
static late final Box user;
static late final Box recVideo;
static late final Box userInfo;
static late final Box hotKeyword;
static Future<void> init() async {
final dir = await getApplicationDocumentsDirectory();
@ -21,6 +23,8 @@ class GStrorage {
recVideo = await Hive.openBox('recVideo');
// 登录用户信息
userInfo = await Hive.openBox('userInfo');
// 热搜关键词
hotKeyword = await Hive.openBox('hotKeyword');
}
static regAdapter() {
@ -30,6 +34,8 @@ class GStrorage {
Hive.registerAdapter(OwnerAdapter());
Hive.registerAdapter(UserInfoDataAdapter());
Hive.registerAdapter(LevelInfoAdapter());
Hive.registerAdapter(HotSearchModelAdapter());
Hive.registerAdapter(HotSearchItemAdapter());
}
}