diff --git a/lib/http/api.dart b/lib/http/api.dart index 532ca341..77694de7 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -214,6 +214,9 @@ class Api { // https://api.bilibili.com/x/relation/tags static const String followingsClass = '/x/relation/tags'; + // 搜索follow + static const followSearch = '/x/relation/followings/search'; + // 粉丝 // vmid 用户id pn 页码 ps 每页个数,最大50 order: desc // order_type 排序规则 最近访问传空,最常访问传 attention diff --git a/lib/http/member.dart b/lib/http/member.dart index bf84b6eb..6b6df7fe 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -461,4 +461,41 @@ class MemberHttp { }; } } + + // 搜索follow + static Future getfollowSearch({ + required int mid, + required int ps, + required int pn, + required String name, + }) async { + Map data = { + 'vmid': mid, + 'pn': pn, + 'ps': ps, + 'order': 'desc', + 'order_type': 'attention', + 'gaia_source': 'main_web', + 'name': name, + 'web_location': 333.999, + }; + Map params = await WbiSign().makSign(data); + var res = await Request().get(Api.followSearch, data: { + ...data, + 'w_rid': params['w_rid'], + 'wts': params['wts'], + }); + if (res.data['code'] == 0) { + return { + 'status': true, + 'data': FollowDataModel.fromJson(res.data['data']) + }; + } else { + return { + 'status': false, + 'data': [], + 'msg': res.data['message'], + }; + } + } } diff --git a/lib/pages/follow/view.dart b/lib/pages/follow/view.dart index a9fcab4e..9633e7f0 100644 --- a/lib/pages/follow/view.dart +++ b/lib/pages/follow/view.dart @@ -37,6 +37,29 @@ class _FollowPageState extends State { : '${_followController.name}的关注', style: Theme.of(context).textTheme.titleMedium, ), + actions: [ + IconButton( + onPressed: () => Get.toNamed('/followSearch?mid=$mid'), + icon: const Icon(Icons.search_outlined), + ), + PopupMenuButton( + icon: const Icon(Icons.more_vert), + itemBuilder: (BuildContext context) => [ + PopupMenuItem( + onTap: () => Get.toNamed('/blackListPage'), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.block, size: 19), + SizedBox(width: 10), + Text('黑名单管理'), + ], + ), + ) + ], + ), + const SizedBox(width: 6), + ], ), body: Obx( () => !_followController.isOwner.value @@ -87,3 +110,22 @@ class _FollowPageState extends State { ); } } + +class _FakeAPI { + static const List _kOptions = [ + 'aardvark', + 'bobcat', + 'chameleon', + ]; + // Searches the options, but injects a fake "network" delay. + static Future> search(String query) async { + await Future.delayed( + const Duration(seconds: 1)); // Fake 1 second delay. + if (query == '') { + return const Iterable.empty(); + } + return _kOptions.where((String option) { + return option.contains(query.toLowerCase()); + }); + } +} diff --git a/lib/pages/follow/widgets/follow_item.dart b/lib/pages/follow/widgets/follow_item.dart index ac9cc01b..d21a89bc 100644 --- a/lib/pages/follow/widgets/follow_item.dart +++ b/lib/pages/follow/widgets/follow_item.dart @@ -42,7 +42,7 @@ class FollowItem extends StatelessWidget { overflow: TextOverflow.ellipsis, ), dense: true, - trailing: ctr!.isOwner.value + trailing: ctr != null && ctr!.isOwner.value ? SizedBox( height: 34, child: TextButton( diff --git a/lib/pages/follow_search/controller.dart b/lib/pages/follow_search/controller.dart new file mode 100644 index 00000000..9fd1590d --- /dev/null +++ b/lib/pages/follow_search/controller.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/http/member.dart'; + +import '../../models/follow/result.dart'; + +class FollowSearchController extends GetxController { + Rx controller = TextEditingController().obs; + final FocusNode searchFocusNode = FocusNode(); + RxString searchKeyWord = ''.obs; + String hintText = '搜索'; + RxString loadingStatus = 'init'.obs; + late int mid = 1; + RxString uname = ''.obs; + int ps = 20; + int pn = 1; + RxList followList = [].obs; + RxInt total = 0.obs; + + @override + void onInit() { + super.onInit(); + mid = int.parse(Get.parameters['mid']!); + } + + // 清空搜索 + void onClear() { + if (searchKeyWord.value.isNotEmpty && controller.value.text != '') { + controller.value.clear(); + searchKeyWord.value = ''; + } else { + Get.back(); + } + } + + void onChange(value) { + searchKeyWord.value = value; + } + + // 提交搜索内容 + void submit() { + loadingStatus.value = 'loading'; + searchFollow(); + } + + Future searchFollow({type = 'init'}) async { + if (controller.value.text == '') { + return {'status': true, 'data': [].obs}; + } + if (type == 'init') { + ps = 1; + } + var res = await MemberHttp.getfollowSearch( + mid: mid, + ps: ps, + pn: pn, + name: controller.value.text, + ); + if (res['status']) { + if (type == 'init') { + followList.value = res['data'].list; + } else { + followList.addAll(res['data'].list); + } + total.value = res['data'].total; + } + return res; + } + + void onLoad() { + searchFollow(type: 'onLoad'); + } +} diff --git a/lib/pages/follow_search/index.dart b/lib/pages/follow_search/index.dart new file mode 100644 index 00000000..805d8c47 --- /dev/null +++ b/lib/pages/follow_search/index.dart @@ -0,0 +1,4 @@ +library follow_search; + +export './controller.dart'; +export './view.dart'; diff --git a/lib/pages/follow_search/view.dart b/lib/pages/follow_search/view.dart new file mode 100644 index 00000000..6be42676 --- /dev/null +++ b/lib/pages/follow_search/view.dart @@ -0,0 +1,121 @@ +import 'package:easy_debounce/easy_throttle.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import 'package:pilipala/common/widgets/http_error.dart'; +import 'package:pilipala/pages/follow_search/index.dart'; + +import '../follow/widgets/follow_item.dart'; + +class FollowSearchPage extends StatefulWidget { + const FollowSearchPage({super.key}); + + @override + State createState() => _FollowSearchPageState(); +} + +class _FollowSearchPageState extends State { + final FollowSearchController _followSearchController = + Get.put(FollowSearchController()); + late Future? _futureBuilder; + final ScrollController scrollController = ScrollController(); + + @override + void initState() { + super.initState(); + _futureBuilder = _followSearchController.searchFollow(); + scrollController.addListener( + () { + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent - 200) { + EasyThrottle.throttle( + 'my-throttler', const Duration(milliseconds: 500), () { + _followSearchController.onLoad(); + }); + } + }, + ); + } + + void reRequest() { + setState(() { + _futureBuilder = _followSearchController.searchFollow(); + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + titleSpacing: 0, + actions: [ + IconButton( + onPressed: reRequest, + icon: const Icon(CupertinoIcons.search, size: 22), + ), + const SizedBox(width: 6), + ], + title: TextField( + autofocus: true, + focusNode: _followSearchController.searchFocusNode, + controller: _followSearchController.controller.value, + textInputAction: TextInputAction.search, + onChanged: (value) => _followSearchController.onChange(value), + decoration: InputDecoration( + hintText: _followSearchController.hintText, + border: InputBorder.none, + suffixIcon: IconButton( + icon: Icon( + Icons.clear, + size: 22, + color: Theme.of(context).colorScheme.outline, + ), + onPressed: () => _followSearchController.onClear(), + ), + ), + onSubmitted: (String value) => reRequest(), + ), + ), + body: FutureBuilder( + future: _futureBuilder, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + var data = snapshot.data; + if (data == null) { + return CustomScrollView( + slivers: [ + HttpError(errMsg: snapshot.data['msg'], fn: reRequest) + ], + ); + } + if (data['status']) { + RxList followList = _followSearchController.followList; + return Obx( + () => followList.isNotEmpty + ? ListView.builder( + controller: scrollController, + itemCount: followList.length, + itemBuilder: ((context, index) { + return FollowItem( + item: followList[index], + ); + }), + ) + : CustomScrollView( + slivers: [HttpError(errMsg: '未搜索到结果', fn: reRequest)], + ), + ); + } else { + return CustomScrollView( + slivers: [ + HttpError(errMsg: snapshot.data['msg'], fn: reRequest) + ], + ); + } + } else { + return const SizedBox(); + } + }), + ); + } +} diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index ff506630..45d7cad1 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:hive/hive.dart'; +import 'package:pilipala/pages/follow_search/view.dart'; import 'package:pilipala/pages/setting/pages/logs.dart'; import '../pages/about/index.dart'; @@ -104,7 +105,8 @@ class Routes { CustomGetPage( name: '/replyReply', page: () => const VideoReplyReplyPanel()), // 推荐设置 - CustomGetPage(name: '/recommendSetting', page: () => const RecommendSetting()), + CustomGetPage( + name: '/recommendSetting', page: () => const RecommendSetting()), // 播放设置 CustomGetPage(name: '/playSetting', page: () => const PlaySetting()), // 外观设置 @@ -156,6 +158,8 @@ class Routes { name: '/memberSeasons', page: () => const MemberSeasonsPage()), // 日志 CustomGetPage(name: '/logs', page: () => const LogsPage()), + // 搜索关注 + CustomGetPage(name: '/followSearch', page: () => const FollowSearchPage()), ]; }