From dccb5d4bf5e9a04a15757d7d4a74df280619ff95 Mon Sep 17 00:00:00 2001 From: dom Date: Sun, 26 Apr 2026 20:36:59 +0800 Subject: [PATCH] create fav tag from fav panel Signed-off-by: dom --- lib/http/member.dart | 4 +- lib/models/member/tags.dart | 6 ++ lib/pages/follow/controller.dart | 53 ++++++++------ lib/pages/follow/view.dart | 117 ++++++++++++++----------------- lib/pages/group_panel/view.dart | 44 +++++++++--- lib/utils/request_utils.dart | 49 ++++++++++--- 6 files changed, 163 insertions(+), 110 deletions(-) diff --git a/lib/http/member.dart b/lib/http/member.dart index 6cb090333..e6fc13b83 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -593,7 +593,7 @@ abstract final class MemberHttp { } } - static Future> createFollowTag(Object tagName) async { + static Future> createFollowTag(String tagName) async { final res = await Request().post( Api.createFollowTag, queryParameters: { @@ -607,7 +607,7 @@ abstract final class MemberHttp { options: Options(contentType: Headers.formUrlEncodedContentType), ); if (res.data['code'] == 0) { - return const Success(null); + return Success(res.data['data']['tagid']); } else { return Error(res.data['message']); } diff --git a/lib/models/member/tags.dart b/lib/models/member/tags.dart index e6c9c4daa..3476d9eed 100644 --- a/lib/models/member/tags.dart +++ b/lib/models/member/tags.dart @@ -17,4 +17,10 @@ class MemberTagItemModel { tagid = json['tagid']; tip = json['tip']; } + + MemberTagItemModel.fromCreate(({int tagid, String tagName}) res) { + tagid = res.tagid; + name = res.tagName; + count = 0; + } } diff --git a/lib/pages/follow/controller.dart b/lib/pages/follow/controller.dart index b803ed8a1..095d05155 100644 --- a/lib/pages/follow/controller.dart +++ b/lib/pages/follow/controller.dart @@ -45,36 +45,35 @@ class FollowController extends GetxController with GetTickerProviderStateMixin { tabs ..assign(MemberTagItemModel(name: '全部关注')) ..addAll(response); - int initialIndex = 0; - if (tabController != null) { - initialIndex = tabController!.index.clamp(0, tabs.length - 1); - tabController!.dispose(); - } - tabController = TabController( - initialIndex: initialIndex, - length: tabs.length, - vsync: this, - ); + onInitTab(); followState.value = Success(tabs.hashCode); } else { followState.value = res; } } - @override - void onClose() { - tabController?.dispose(); - super.onClose(); + void onInitTab() { + int initialIndex = 0; + if (tabController != null) { + initialIndex = tabController!.index.clamp(0, tabs.length - 1); + tabController!.dispose(); + } + tabController = TabController( + initialIndex: initialIndex, + length: tabs.length, + vsync: this, + ); } - Future onCreateTag(String tagName) async { - final res = await MemberHttp.createFollowTag(tagName); - if (res.isSuccess) { + void onCreateFavTag(({int tagid, String tagName}) res) { + if (isClosed) return; + if (followState.value.isSuccess) { + tabs.add(MemberTagItemModel.fromCreate(res)); + onInitTab(); + followState.refresh(); + } else { followState.value = LoadingState.loading(); queryFollowUpTags(); - SmartDialog.showToast('创建成功'); - } else { - res.toast(); } } @@ -89,14 +88,22 @@ class FollowController extends GetxController with GetTickerProviderStateMixin { } } - Future onDelTag(int tagid) async { + Future onDelTag(int index, int tagid) async { final res = await MemberHttp.delFollowTag(tagid); if (res.isSuccess) { - followState.value = LoadingState.loading(); - queryFollowUpTags(); + tabs.removeAt(index); + onInitTab(); + followState.refresh(); SmartDialog.showToast('删除成功'); } else { res.toast(); } } + + @override + void onClose() { + tabController?.dispose(); + tabController = null; + super.onClose(); + } } diff --git a/lib/pages/follow/view.dart b/lib/pages/follow/view.dart index 3401b387d..b7c4ff957 100644 --- a/lib/pages/follow/view.dart +++ b/lib/pages/follow/view.dart @@ -8,6 +8,7 @@ import 'package:PiliPlus/pages/follow/child/child_controller.dart'; import 'package:PiliPlus/pages/follow/child/child_view.dart'; import 'package:PiliPlus/pages/follow/controller.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:PiliPlus/utils/request_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show LengthLimitingTextInputFormatter; @@ -45,57 +46,62 @@ class _FollowPageState extends State { Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, - appBar: AppBar( - title: _followController.isOwner - ? const Text('我的关注') - : Obx(() { - final name = _followController.name.value; - if (name != null) return Text('$name的关注'); - return const SizedBox.shrink(); - }), - actions: _followController.isOwner - ? [ - IconButton( - onPressed: _onCreateTag, - icon: const Icon(Icons.add), - tooltip: '新建分组', - ), - IconButton( - onPressed: () => Get.toNamed( - '/followSearch', - arguments: { - 'mid': _followController.mid, - }, - ), - icon: const Icon(Icons.search_outlined), - tooltip: '搜索', - ), - PopupMenuButton( - icon: const Icon(Icons.more_vert), - itemBuilder: (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), - ] - : null, - ), + appBar: _buildAppBar, body: _followController.isOwner ? Obx(() => _buildBody(_followController.followState.value)) : _childPage(), ); } + PreferredSizeWidget get _buildAppBar => AppBar( + title: _followController.isOwner + ? const Text('我的关注') + : Obx(() { + final name = _followController.name.value; + if (name != null) return Text('$name的关注'); + return const SizedBox.shrink(); + }), + actions: _followController.isOwner + ? [ + IconButton( + onPressed: () => RequestUtils.createFavTag( + context, + _followController.onCreateFavTag, + ), + icon: const Icon(Icons.add), + tooltip: '新建分组', + ), + IconButton( + onPressed: () => Get.toNamed( + '/followSearch', + arguments: { + 'mid': _followController.mid, + }, + ), + icon: const Icon(Icons.search_outlined), + tooltip: '搜索', + ), + PopupMenuButton( + icon: const Icon(Icons.more_vert), + itemBuilder: (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), + ] + : null, + ); + Widget _childPage([MemberTagItemModel? item]) => FollowChildPage( tag: _tag, controller: _followController, @@ -223,7 +229,8 @@ class _FollowPageState extends State { context: context, title: const Text('删除分组'), content: const Text('删除后,该分组下的用户依旧保留?'), - onConfirm: () => _followController.onDelTag(item.tagid!), + onConfirm: () => + _followController.onDelTag(index, item.tagid!), ); }, dense: true, @@ -237,22 +244,4 @@ class _FollowPageState extends State { ), ); } - - void _onCreateTag() { - String tagName = ''; - showConfirmDialog( - context: context, - title: const Text('新建分组'), - content: TextFormField( - autofocus: true, - initialValue: tagName, - onChanged: (value) => tagName = value, - inputFormatters: [ - LengthLimitingTextInputFormatter(16), - ], - decoration: const InputDecoration(border: OutlineInputBorder()), - ), - onConfirm: () => _followController.onCreateTag(tagName), - ); - } } diff --git a/lib/pages/group_panel/view.dart b/lib/pages/group_panel/view.dart index 921e41e25..0cd9b5f5a 100644 --- a/lib/pages/group_panel/view.dart +++ b/lib/pages/group_panel/view.dart @@ -5,6 +5,7 @@ import 'package:PiliPlus/models/member/tags.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/feed_back.dart'; +import 'package:PiliPlus/utils/request_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -26,7 +27,7 @@ class GroupPanel extends StatefulWidget { class _GroupPanelState extends State { LoadingState> loadingState = LoadingState.loading(); - RxBool showDefaultBtn = true.obs; + final RxBool showDefaultBtn = true.obs; late final Set tags = widget.tags == null ? {} : Set.from(widget.tags!); @@ -34,10 +35,10 @@ class _GroupPanelState extends State { @override void initState() { super.initState(); - _query(); + _queryFollowUpTags(); } - void _query() { + void _queryFollowUpTags() { MemberHttp.followUpTags().then((res) { if (mounted) { loadingState = res..dataOrNull?.removeFirstWhere((e) => e.tagid == 0); @@ -116,7 +117,7 @@ class _GroupPanelState extends State { Error(:final errMsg) => scrollErrorWidget( controller: widget.scrollController, errMsg: errMsg, - onReload: _query, + onReload: _queryFollowUpTags, ), }; } @@ -125,8 +126,8 @@ class _GroupPanelState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); return Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ + crossAxisAlignment: .end, + children: [ AppBar( backgroundColor: Colors.transparent, leading: IconButton( @@ -135,6 +136,21 @@ class _GroupPanelState extends State { icon: const Icon(Icons.close_outlined), ), title: const Text('设置关注分组'), + actions: [ + TextButton.icon( + onPressed: () => + RequestUtils.createFavTag(context, _onCreateFavTag), + icon: Icon(Icons.add, color: theme.colorScheme.primary), + label: const Text('新建分组'), + style: const ButtonStyle( + visualDensity: .compact, + padding: WidgetStatePropertyAll( + .symmetric(horizontal: 18, vertical: 14), + ), + ), + ), + const SizedBox(width: 16), + ], ), Expanded(child: _buildBody), Divider( @@ -142,20 +158,28 @@ class _GroupPanelState extends State { color: theme.disabledColor.withValues(alpha: 0.08), ), Padding( - padding: EdgeInsets.only( + padding: .only( right: 20, top: 12, bottom: MediaQuery.viewPaddingOf(context).bottom + 12, ), child: FilledButton.tonal( onPressed: onSave, - style: FilledButton.styleFrom( - visualDensity: VisualDensity.compact, - ), + style: const ButtonStyle(visualDensity: .compact), child: Obx(() => Text(showDefaultBtn.value ? '保存至默认分组' : '保存')), ), ), ], ); } + + void _onCreateFavTag(({int tagid, String tagName}) res) { + if (!mounted) return; + if (loadingState case Success(:final response)) { + response.add(MemberTagItemModel.fromCreate(res)); + setState(() {}); + } else { + _queryFollowUpTags(); + } + } } diff --git a/lib/utils/request_utils.dart b/lib/utils/request_utils.dart index 9d68cf03d..ad763ea2f 100644 --- a/lib/utils/request_utils.dart +++ b/lib/utils/request_utils.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:math'; +import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/grpc/bilibili/im/type.pbenum.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo; @@ -37,6 +38,7 @@ import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show LengthLimitingTextInputFormatter; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart'; @@ -99,6 +101,35 @@ abstract final class RequestUtils { } } + static Future createFavTag( + BuildContext context, + ValueChanged<({int tagid, String tagName})> onSuccess, + ) async { + String tagName = ''; + final onCreate = await showConfirmDialog( + context: context, + title: const Text('新建分组'), + content: TextFormField( + autofocus: true, + initialValue: tagName, + onChanged: (value) => tagName = value, + inputFormatters: [ + LengthLimitingTextInputFormatter(16), + ], + decoration: const InputDecoration(border: OutlineInputBorder()), + ), + ); + if (onCreate) { + final res = await MemberHttp.createFollowTag(tagName); + if (res case Success(:final response)) { + onSuccess((tagid: response, tagName: tagName)); + SmartDialog.showToast('创建成功'); + } else { + res.toast(); + } + } + } + static Future actionRelationMod({ required BuildContext context, required dynamic mid, @@ -188,17 +219,13 @@ abstract final class RequestUtils { expand: false, snapSizes: [maxChildSize], initialChildSize: maxChildSize, - builder: - ( - BuildContext context, - ScrollController scrollController, - ) { - return GroupPanel( - mid: mid, - tags: followStatus!.tag, - scrollController: scrollController, - ); - }, + builder: (context, scrollController) { + return GroupPanel( + mid: mid, + tags: followStatus!.tag, + scrollController: scrollController, + ); + }, ); }, );