diff --git a/lib/http/api.dart b/lib/http/api.dart index 88a5ad026..125c5ab01 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -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'; } diff --git a/lib/http/index.dart b/lib/http/index.dart new file mode 100644 index 000000000..84ff1d311 --- /dev/null +++ b/lib/http/index.dart @@ -0,0 +1,2 @@ +export 'api.dart'; +export 'init.dart'; diff --git a/lib/http/search.dart b/lib/http/search.dart new file mode 100644 index 000000000..d4331ac16 --- /dev/null +++ b/lib/http/search.dart @@ -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': '请求错误 🙅', + }; + } + } +} diff --git a/lib/models/search/hot.dart b/lib/models/search/hot.dart new file mode 100644 index 000000000..ce09b2eab --- /dev/null +++ b/lib/models/search/hot.dart @@ -0,0 +1,46 @@ +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'] + .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 new file mode 100644 index 000000000..a06dd4756 --- /dev/null +++ b/lib/models/search/hot.g.dart @@ -0,0 +1,84 @@ +// 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/pages/home/widgets/app_bar.dart b/lib/pages/home/widgets/app_bar.dart index da890b78f..c9caf7db3 100644 --- a/lib/pages/home/widgets/app_bar.dart +++ b/lib/pages/home/widgets/app_bar.dart @@ -37,7 +37,9 @@ class HomeAppBar extends StatelessWidget { ), actions: [ IconButton( - onPressed: () {}, + onPressed: () { + Get.toNamed('/search'); + }, icon: const Icon(CupertinoIcons.search, size: 22), ), // IconButton( diff --git a/lib/pages/search/controller.dart b/lib/pages/search/controller.dart new file mode 100644 index 000000000..848c4d110 --- /dev/null +++ b/lib/pages/search/controller.dart @@ -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 controller = TextEditingController().obs; + List tabs = [ + {'label': '综合', 'id': ''}, + {'label': '视频', 'id': ''}, + {'label': '番剧', 'id': ''}, + {'label': '直播', 'id': ''}, + {'label': '专栏', 'id': ''}, + {'label': '用户', 'id': ''} + ]; + List hotSearchList = []; + Box hotKeyword = GStrorage.hotKeyword; + + @override + void onInit() { + super.onInit(); + if (hotKeyword.get('cacheList') != null && + hotKeyword.get('cacheList').isNotEmpty) { + List 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), + ); + } +} diff --git a/lib/pages/search/index.dart b/lib/pages/search/index.dart new file mode 100644 index 000000000..b6847050b --- /dev/null +++ b/lib/pages/search/index.dart @@ -0,0 +1,4 @@ +library search; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/search/view.dart b/lib/pages/search/view.dart new file mode 100644 index 000000000..d6c550df8 --- /dev/null +++ b/lib/pages/search/view.dart @@ -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 createState() => _SearchPageState(); +} + +class _SearchPageState extends State { + 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(); + } + } + }, + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/pages/search/widgets/hotKeyword.dart b/lib/pages/search/widgets/hotKeyword.dart new file mode 100644 index 000000000..30c2e52a0 --- /dev/null +++ b/lib/pages/search/widgets/hotKeyword.dart @@ -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, + ), + ), + ], + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/video/detail/reply/widgets/reply_item.dart b/lib/pages/video/detail/reply/widgets/reply_item.dart index d79738c7b..f211efbfe 100644 --- a/lib/pages/video/detail/reply/widgets/reply_item.dart +++ b/lib/pages/video/detail/reply/widgets/reply_item.dart @@ -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( diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 7e6c1a456..095e4e8b2 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -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()) ]; } diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index 9860cb3fd..089eada7f 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -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 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()); } }