From 44bd5afb707a8ff513bb5cca8603b317d8fbac97 Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Sat, 23 Nov 2024 22:05:06 +0800 Subject: [PATCH] refactor: search page Signed-off-by: bggRGjQaUbCoE --- lib/common/widgets/dynamic_sliver_appbar.dart | 2 +- lib/common/widgets/network_img_layer.dart | 3 +- lib/http/search.dart | 2 +- lib/main.dart | 2 - .../member/new/widget/edit_profile_page.dart | 1 + lib/pages/search/controller.dart | 117 ++++----- lib/pages/search/view.dart | 247 ++++++++---------- lib/pages/search/widgets/search_text.dart | 2 - 8 files changed, 157 insertions(+), 219 deletions(-) diff --git a/lib/common/widgets/dynamic_sliver_appbar.dart b/lib/common/widgets/dynamic_sliver_appbar.dart index 18173d95..a283209f 100644 --- a/lib/common/widgets/dynamic_sliver_appbar.dart +++ b/lib/common/widgets/dynamic_sliver_appbar.dart @@ -86,7 +86,7 @@ class DynamicSliverAppBar extends StatefulWidget { final CustomClipper? appBarClipper; @override - _DynamicSliverAppBarState createState() => _DynamicSliverAppBarState(); + State createState() => _DynamicSliverAppBarState(); } class _DynamicSliverAppBarState extends State { diff --git a/lib/common/widgets/network_img_layer.dart b/lib/common/widgets/network_img_layer.dart index 786e15e4..832e46d5 100644 --- a/lib/common/widgets/network_img_layer.dart +++ b/lib/common/widgets/network_img_layer.dart @@ -67,8 +67,7 @@ class NetworkImgLayer extends StatelessWidget { child: Builder( builder: (context) => CachedNetworkImage( imageUrl: - '${src?.startsWith('//') == true ? 'https:$src' : src}${thumbnail ? '@${quality ?? defaultImgQuality}q.webp' : ''}' - .http2https, + '${src?.startsWith('//') == true ? 'https:$src' : src?.http2https}${thumbnail ? '@${quality ?? defaultImgQuality}q.webp' : ''}', width: width, height: ignoreHeight == null || ignoreHeight == false ? height diff --git a/lib/http/search.dart b/lib/http/search.dart index 23c18b2f..f8f37d2c 100644 --- a/lib/http/search.dart +++ b/lib/http/search.dart @@ -33,7 +33,7 @@ class SearchHttp { return { 'status': false, 'data': [], - 'msg': '请求错误 🙅', + 'msg': '请求错误', }; } diff --git a/lib/main.dart b/lib/main.dart index cbced725..bbe34a8c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,7 +12,6 @@ import 'package:hive/hive.dart'; import 'package:PiliPalaX/common/widgets/custom_toast.dart'; import 'package:PiliPalaX/http/init.dart'; import 'package:PiliPalaX/models/common/color_type.dart'; -import 'package:PiliPalaX/pages/search/index.dart'; import 'package:PiliPalaX/pages/video/detail/index.dart'; import 'package:PiliPalaX/router/app_pages.dart'; import 'package:PiliPalaX/pages/main/view.dart'; @@ -190,7 +189,6 @@ class MyApp extends StatelessWidget { }, navigatorObservers: [ VideoDetailPage.routeObserver, - SearchPage.routeObserver, MainApp.routeObserver, ], ); diff --git a/lib/pages/member/new/widget/edit_profile_page.dart b/lib/pages/member/new/widget/edit_profile_page.dart index 2eaabf80..8b729337 100644 --- a/lib/pages/member/new/widget/edit_profile_page.dart +++ b/lib/pages/member/new/widget/edit_profile_page.dart @@ -214,6 +214,7 @@ class _EditProfilePageState extends State { 'https://account.bilibili.com/official/mobile/home'), ), _divider, + SizedBox(height: 25 + MediaQuery.paddingOf(context).bottom), ], ), ), diff --git a/lib/pages/search/controller.dart b/lib/pages/search/controller.dart index c9533de9..feff050f 100644 --- a/lib/pages/search/controller.dart +++ b/lib/pages/search/controller.dart @@ -1,27 +1,23 @@ +import 'package:PiliPalaX/http/loading_state.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:get/get_rx/src/rx_workers/utils/debouncer.dart'; -import 'package:hive/hive.dart'; import 'package:PiliPalaX/http/search.dart'; -import 'package:PiliPalaX/models/search/hot.dart'; import 'package:PiliPalaX/models/search/suggest.dart'; import 'package:PiliPalaX/utils/storage.dart'; class SSearchController extends GetxController { - final FocusNode searchFocusNode = FocusNode(); - RxString searchKeyWord = ''.obs; - Rx controller = TextEditingController().obs; - RxList hotSearchList = [].obs; - Box historyWord = GStorage.historyWord; - List historyCacheList = []; + final searchFocusNode = FocusNode(); + final controller = TextEditingController(); + RxList historyList = [].obs; + RxList searchSuggestList = [].obs; - final _debouncer = - Debouncer(delay: const Duration(milliseconds: 200)); // 设置延迟时间 + String hintText = '搜索'; - RxString defaultSearch = ''.obs; - Box setting = GStorage.setting; - bool enableHotKey = true; + + late bool enableHotKey; + + Rx loadingState = LoadingState.loading().obs; @override void onInit() { @@ -33,28 +29,31 @@ class SSearchController extends GetxController { } if (Get.parameters['hintText'] != null) { hintText = Get.parameters['hintText']!; - searchKeyWord.value = hintText; } } - historyCacheList = List.from(historyWord.get('cacheList') ?? []); - historyList.value = historyCacheList; - enableHotKey = setting.get(SettingBoxKey.enableHotKey, defaultValue: true); + + historyList.value = List.from(GStorage.historyWord.get('cacheList') ?? []); + + enableHotKey = + GStorage.setting.get(SettingBoxKey.enableHotKey, defaultValue: true); + + if (enableHotKey) { + queryHotSearchList(); + } } - void onChange(value) { - searchKeyWord.value = value; - if (value == '') { - searchSuggestList.value = []; - return; + void onChange(String value) { + if (value.isEmpty) { + searchSuggestList.clear(); + } else { + querySearchSuggest(value); } - _debouncer.call(() => querySearchSuggest(value)); } void onClear() { - if (searchKeyWord.value.isNotEmpty && controller.value.text != '') { - controller.value.clear(); - searchKeyWord.value = ''; - searchSuggestList.value = []; + if (controller.value.text != '') { + controller.clear(); + searchSuggestList.clear(); searchFocusNode.requestFocus(); } else { Get.back(); @@ -62,44 +61,38 @@ class SSearchController extends GetxController { } // 搜索 - void submit() { - // ignore: unrelated_type_equality_checks - if (searchKeyWord == '') { - if (hintText == '') { + void submit() async { + if (controller.text.isEmpty) { + if (hintText.isEmpty) { return; } - searchKeyWord.value = hintText; + controller.text = hintText; } - List arr = - historyCacheList.where((e) => e != searchKeyWord.value).toList(); - arr.insert(0, searchKeyWord.value); - historyCacheList = arr; - historyList.value = historyCacheList; - // 手动刷新 - historyList.refresh(); - historyWord.put('cacheList', historyCacheList); + historyList.remove(controller.text); + historyList.insert(0, controller.text); + GStorage.historyWord.put('cacheList', historyList); searchFocusNode.unfocus(); - Get.toNamed('/searchResult', parameters: {'keyword': searchKeyWord.value}); + + await Get.toNamed('/searchResult', parameters: { + 'keyword': controller.text, + }); + searchFocusNode.requestFocus(); } // 获取热搜关键词 Future queryHotSearchList() async { - var result = await SearchHttp.hotSearchList(); + dynamic result = await SearchHttp.hotSearchList(); if (result['status']) { - hotSearchList.value = result['data'].list; + loadingState.value = LoadingState.success(result['data'].list); + } else { + loadingState.value = LoadingState.error(result['msg']); } - return result; } - // 点击热搜关键词 void onClickKeyword(String keyword) { - searchKeyWord.value = keyword; - controller.value.text = keyword; - // 移动光标 - controller.value.selection = TextSelection.fromPosition( - TextPosition(offset: controller.value.text.length), - ); + controller.text = keyword; + searchSuggestList.clear(); submit(); } @@ -112,28 +105,20 @@ class SSearchController extends GetxController { } } - onSelect(word) { - searchKeyWord.value = word; - controller.value.text = word; - submit(); - } - onLongSelect(word) { - int index = historyList.indexOf(word); - historyList.removeAt(index); - historyWord.put('cacheList', historyList); + historyList.remove(word); + GStorage.historyWord.put('cacheList', historyList); } - onClearHis() { - historyList.value = []; - historyCacheList = []; - historyList.refresh(); - historyWord.put('cacheList', []); + onClearHistory() { + historyList.clear(); + GStorage.historyWord.put('cacheList', []); } @override void onClose() { searchFocusNode.dispose(); + controller.dispose(); super.onClose(); } } diff --git a/lib/pages/search/view.dart b/lib/pages/search/view.dart index fd984667..88f45ce2 100644 --- a/lib/pages/search/view.dart +++ b/lib/pages/search/view.dart @@ -1,6 +1,7 @@ +import 'package:PiliPalaX/common/widgets/loading_widget.dart'; +import 'package:PiliPalaX/http/loading_state.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:PiliPalaX/common/widgets/http_error.dart'; import 'controller.dart'; import 'widgets/hot_keyword.dart'; import 'widgets/search_text.dart'; @@ -10,39 +11,10 @@ class SearchPage extends StatefulWidget { @override State createState() => _SearchPageState(); - static final RouteObserver routeObserver = - RouteObserver(); } class _SearchPageState extends State with RouteAware { final SSearchController _searchController = Get.put(SSearchController()); - late Future? _futureBuilderFuture; - - @override - void initState() { - super.initState(); - _futureBuilderFuture = _searchController.queryHotSearchList(); - } - - @override - void dispose() { - SearchPage.routeObserver.unsubscribe(this); - super.dispose(); - } - - @override - // 返回当前页面时 - void didPopNext() async { - _searchController.searchFocusNode.requestFocus(); - super.didPopNext(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - SearchPage.routeObserver - .subscribe(this, ModalRoute.of(context) as PageRoute); - } @override Widget build(BuildContext context) { @@ -63,38 +35,33 @@ class _SearchPageState extends State with RouteAware { ), const SizedBox(width: 10) ], - title: Obx( - () => TextField( - autofocus: true, - focusNode: _searchController.searchFocusNode, - controller: _searchController.controller.value, - textInputAction: TextInputAction.search, - onChanged: (value) => _searchController.onChange(value), - textAlignVertical: TextAlignVertical.center, - decoration: InputDecoration( - hintText: _searchController.hintText, - border: InputBorder.none, - suffixIcon: IconButton( - tooltip: '清空', - icon: const Icon(Icons.clear, size: 22), - onPressed: () => _searchController.onClear(), - ), + title: TextField( + autofocus: true, + focusNode: _searchController.searchFocusNode, + controller: _searchController.controller, + textInputAction: TextInputAction.search, + onChanged: _searchController.onChange, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + hintText: _searchController.hintText, + border: InputBorder.none, + suffixIcon: IconButton( + tooltip: '清空', + icon: const Icon(Icons.clear, size: 22), + onPressed: _searchController.onClear, ), - onSubmitted: (String value) => _searchController.submit(), ), + onSubmitted: (value) => _searchController.submit(), ), ), body: SingleChildScrollView( child: Column( children: [ - const SizedBox(height: 12), // 搜索建议 _searchSuggest(), - // 热搜 - Visibility( - visible: _searchController.enableHotKey, - child: hotSearch(_searchController), - ), + if (_searchController.enableHotKey) + // 热搜 + hotSearch(), // 搜索历史 _history() ], @@ -107,32 +74,37 @@ class _SearchPageState extends State with RouteAware { return Obx( () => _searchController.searchSuggestList.isNotEmpty && _searchController.searchSuggestList.first.term != null && - _searchController.controller.value.text != '' - ? ListView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: _searchController.searchSuggestList.length, - itemBuilder: (context, index) { - return InkWell( - customBorder: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), - onTap: () => _searchController.onClickKeyword( - _searchController.searchSuggestList[index].term!), - child: Padding( - padding: const EdgeInsets.only(left: 20, top: 9, bottom: 9), - child: _searchController.searchSuggestList[index].textRich, - ), - ); - }, + _searchController.controller.text != '' + ? Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: _searchController.searchSuggestList + .map( + (item) => InkWell( + customBorder: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + onTap: () => _searchController.onClickKeyword(item.term!), + child: Container( + width: double.infinity, + padding: const EdgeInsets.only( + left: 20, + top: 9, + bottom: 9, + ), + child: item.textRich, + ), + ), + ) + .toList(), ) - : const SizedBox(), + : const SizedBox.shrink(), ); } - Widget hotSearch(ctr) { + Widget hotSearch() { return Padding( - padding: const EdgeInsets.fromLTRB(10, 14, 4, 0), + padding: const EdgeInsets.fromLTRB(10, 25, 4, 25), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -152,10 +124,12 @@ class _SearchPageState extends State with RouteAware { height: 34, child: TextButton.icon( style: ButtonStyle( - padding: WidgetStateProperty.all(const EdgeInsets.only( - left: 10, top: 6, bottom: 6, right: 10)), + padding: WidgetStateProperty.all( + const EdgeInsets.only( + left: 10, top: 6, bottom: 6, right: 10), + ), ), - onPressed: () => ctr.queryHotSearchList(), + onPressed: _searchController.queryHotSearchList, icon: const Icon(Icons.refresh_outlined, size: 18), label: const Text('刷新'), ), @@ -163,60 +137,7 @@ class _SearchPageState extends State with RouteAware { ], ), ), - LayoutBuilder( - builder: (context, boxConstraints) { - final double width = boxConstraints.maxWidth; - return FutureBuilder( - future: _futureBuilderFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (snapshot.data == null) { - return const SizedBox(); - } - Map data = snapshot.data as Map; - if (data['status']) { - return Obx( - () => HotKeyword( - width: width, - // ignore: invalid_use_of_protected_member - hotSearchList: _searchController.hotSearchList.value, - onClick: (keyword) async { - _searchController.searchFocusNode.unfocus(); - await Future.delayed( - const Duration(milliseconds: 150)); - _searchController.onClickKeyword(keyword); - }, - ), - ); - } else { - return CustomScrollView( - shrinkWrap: true, - slivers: [ - HttpError( - errMsg: data['msg'], - callback: () => setState(() { - _futureBuilderFuture = - _searchController.queryHotSearchList(); - }), - ), - ], - ); - } - } else { - // 缓存数据 - if (_searchController.hotSearchList.isNotEmpty) { - return HotKeyword( - width: width, - hotSearchList: _searchController.hotSearchList, - ); - } else { - return const SizedBox(); - } - } - }, - ); - }, - ), + Obx(() => _buildHotKey(_searchController.loadingState.value)), ], ), ); @@ -227,13 +148,17 @@ class _SearchPageState extends State with RouteAware { () => Container( width: double.infinity, padding: EdgeInsets.fromLTRB( - 10, 25, 6, MediaQuery.of(context).padding.bottom + 50), + 10, + _searchController.enableHotKey ? 0 : 6, + 6, + MediaQuery.of(context).padding.bottom + 50, + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (_searchController.historyList.isNotEmpty) Padding( - padding: const EdgeInsets.fromLTRB(6, 0, 0, 2), + padding: const EdgeInsets.fromLTRB(6, 0, 6, 6), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -244,9 +169,23 @@ class _SearchPageState extends State with RouteAware { .titleMedium! .copyWith(fontWeight: FontWeight.bold), ), - TextButton( - onPressed: () => _searchController.onClearHis(), - child: const Text('清空'), + SizedBox( + height: 34, + child: TextButton.icon( + style: ButtonStyle( + padding: WidgetStateProperty.all( + const EdgeInsets.only( + left: 10, + top: 6, + bottom: 6, + right: 10, + ), + ), + ), + onPressed: _searchController.onClearHistory, + icon: const Icon(Icons.clear_all_outlined, size: 18), + label: const Text('清空'), + ), ) ], ), @@ -257,16 +196,15 @@ class _SearchPageState extends State with RouteAware { runSpacing: 8, direction: Axis.horizontal, textDirection: TextDirection.ltr, - children: [ - for (int i = 0; i < _searchController.historyList.length; i++) - SearchText( - searchText: _searchController.historyList[i], - searchTextIdx: i, - onSelect: (value) => _searchController.onSelect(value), - onLongSelect: (value) => - _searchController.onLongSelect(value), + children: _searchController.historyList + .map( + (item) => SearchText( + searchText: item, + onSelect: _searchController.onClickKeyword, + onLongSelect: _searchController.onLongSelect, + ), ) - ], + .toList(), ), ), ], @@ -274,4 +212,23 @@ class _SearchPageState extends State with RouteAware { ), ); } + + Widget _buildHotKey(LoadingState loadingState) { + return switch (loadingState) { + Success() => (loadingState.response as List?)?.isNotEmpty == true + ? LayoutBuilder( + builder: (_, constraints) => HotKeyword( + width: constraints.maxWidth, + hotSearchList: loadingState.response, + onClick: _searchController.onClickKeyword, + ), + ) + : const SizedBox.shrink(), + Error() => errorWidget( + errMsg: loadingState.errMsg, + callback: _searchController.queryHotSearchList, + ), + _ => const SizedBox.shrink(), + }; + } } diff --git a/lib/pages/search/widgets/search_text.dart b/lib/pages/search/widgets/search_text.dart index 501ae100..33c3a41c 100644 --- a/lib/pages/search/widgets/search_text.dart +++ b/lib/pages/search/widgets/search_text.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; class SearchText extends StatelessWidget { final String? searchText; final Function? onSelect; - final int? searchTextIdx; final Function? onLongSelect; final double? fontSize; final Color? bgColor; @@ -13,7 +12,6 @@ class SearchText extends StatelessWidget { super.key, this.searchText, this.onSelect, - this.searchTextIdx, this.onLongSelect, this.fontSize, this.bgColor,