From 0dd4aff9d33ac0bf157bb1e44f1a6d8bd239d367 Mon Sep 17 00:00:00 2001 From: Otaku_Luo Date: Wed, 17 Jun 2026 23:19:07 +0800 Subject: [PATCH 1/2] add search bar for collect page --- lib/pages/collect/collect_controller.dart | 3 + lib/pages/collect/collect_controller.g.dart | 19 +++++- lib/pages/collect/collect_page.dart | 66 ++++++++++++++++++++- 3 files changed, 84 insertions(+), 4 deletions(-) diff --git a/lib/pages/collect/collect_controller.dart b/lib/pages/collect/collect_controller.dart index 063654b62..f63c273fc 100644 --- a/lib/pages/collect/collect_controller.dart +++ b/lib/pages/collect/collect_controller.dart @@ -35,6 +35,9 @@ abstract class _CollectController with Store { ObservableList collectibles = ObservableList(); + @observable + String searchText = ''; + void loadCollectibles() { collectibles.clear(); collectibles.addAll(_collectCrudRepository.getAllCollectibles()); diff --git a/lib/pages/collect/collect_controller.g.dart b/lib/pages/collect/collect_controller.g.dart index 4ed5a1c48..e69aa09d2 100644 --- a/lib/pages/collect/collect_controller.g.dart +++ b/lib/pages/collect/collect_controller.g.dart @@ -25,6 +25,22 @@ mixin _$CollectController on _CollectController, Store { }); } + late final _$searchTextAtom = + Atom(name: '_CollectController.searchText', context: context); + + @override + String get searchText { + _$searchTextAtom.reportRead(); + return super.searchText; + } + + @override + set searchText(String value) { + _$searchTextAtom.reportWrite(value, super.searchText, () { + super.searchText = value; + }); + } + late final _$addCollectAsyncAction = AsyncAction('_CollectController.addCollect', context: context); @@ -46,7 +62,8 @@ mixin _$CollectController on _CollectController, Store { @override String toString() { return ''' -collectibles: ${collectibles} +collectibles: ${collectibles}, +searchText: ${searchText} '''; } } diff --git a/lib/pages/collect/collect_page.dart b/lib/pages/collect/collect_page.dart index 8aad24a39..6cbf78935 100644 --- a/lib/pages/collect/collect_page.dart +++ b/lib/pages/collect/collect_page.dart @@ -25,10 +25,12 @@ class CollectPage extends StatefulWidget { class _CollectPageState extends State with SingleTickerProviderStateMixin { final CollectController collectController = Modular.get(); + final TextEditingController searchController = TextEditingController(); late NavigationBarState navigationBarState; TabController? tabController; bool showDelete = false; bool syncCollectiblesing = false; + bool searchBarHovering = false; Future _syncBangumiWithProgress({ required GlobalKey<_FullSyncProgressDialogState> progressDialogKey, @@ -133,6 +135,7 @@ class _CollectPageState extends State } void onBackPressed(BuildContext context) { + collectController.searchText = ''; if (syncCollectiblesing) { return; } @@ -147,6 +150,7 @@ class _CollectPageState extends State @override void initState() { super.initState(); + searchController.text = collectController.searchText; collectController.loadCollectibles(); tabController = TabController(vsync: this, length: tabs.length); navigationBarState = @@ -156,6 +160,7 @@ class _CollectPageState extends State @override void dispose() { tabController?.dispose(); + searchController.dispose(); super.dispose(); } @@ -246,9 +251,58 @@ class _CollectPageState extends State width: 32, height: 32, child: CircularProgressIndicator()) : const Icon(Icons.sync_rounded), ), - body: Observer(builder: (context) { - return renderBody; - }), + body: Stack( + children: [ + Observer(builder: (context) { + return renderBody; + }), + Positioned( + left: 0, + right:0, + bottom: 16, + child: Align( + alignment: Alignment.bottomCenter, + child: FractionallySizedBox( + widthFactor: 0.5, + child: MouseRegion( + onHover: (_) => setState(() => searchBarHovering = true), + onExit: (_) => setState(() => searchBarHovering = false), + child: AnimatedOpacity( + duration: const Duration(milliseconds: 200), + opacity: searchBarHovering ? 1.0 : 0.5, + child: Material( + elevation: 8, + borderRadius: BorderRadius.circular(24), + child: SearchBar( + controller: searchController, + hintText: '搜索番剧', + leading: ValueListenableBuilder( + valueListenable: searchController, + builder: (context, value, child) { + if (value.text.isEmpty) { + return const Icon(Icons.search); + } + return IconButton( + icon: const Icon(Icons.close), + onPressed: () { + searchController.clear(); + collectController.searchText = ''; + }, + ); + }, + ), + onChanged: (text) { + collectController.searchText = text; + }, + ), + ), + ), + ), + ), + ), + ), + ], + ), ), ); } @@ -274,6 +328,12 @@ class _CollectPageState extends State collectedBangumiRenderItemList[element.type - 1].add(element); } for (List list in collectedBangumiRenderItemList) { + if (collectController.searchText.isNotEmpty) { + list.removeWhere((item) { + return !item.bangumiItem.nameCn.contains(collectController.searchText) && + !item.bangumiItem.name.contains(collectController.searchText); + }); + } list.sort((a, b) => b.time.millisecondsSinceEpoch .compareTo(a.time.millisecondsSinceEpoch)); } From 79c2bd93d137bb1f275b59bc1c4613a5b7e2dd17 Mon Sep 17 00:00:00 2001 From: Otaku_Luo Date: Thu, 18 Jun 2026 21:11:02 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E6=90=9C=E7=B4=A2=E5=85=A5=E5=8F=A3?= =?UTF-8?q?=E6=A0=B7=E5=BC=8F=E5=92=8C=E9=80=BB=E8=BE=91=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pages/collect/collect_controller.dart | 3 + lib/pages/collect/collect_controller.g.dart | 19 ++- lib/pages/collect/collect_page.dart | 158 +++++++++++++------- 3 files changed, 129 insertions(+), 51 deletions(-) diff --git a/lib/pages/collect/collect_controller.dart b/lib/pages/collect/collect_controller.dart index f63c273fc..d801a12ca 100644 --- a/lib/pages/collect/collect_controller.dart +++ b/lib/pages/collect/collect_controller.dart @@ -38,6 +38,9 @@ abstract class _CollectController with Store { @observable String searchText = ''; + @observable + bool isSearching = false; + void loadCollectibles() { collectibles.clear(); collectibles.addAll(_collectCrudRepository.getAllCollectibles()); diff --git a/lib/pages/collect/collect_controller.g.dart b/lib/pages/collect/collect_controller.g.dart index e69aa09d2..983b584cb 100644 --- a/lib/pages/collect/collect_controller.g.dart +++ b/lib/pages/collect/collect_controller.g.dart @@ -41,6 +41,22 @@ mixin _$CollectController on _CollectController, Store { }); } + late final _$isSearchingAtom = + Atom(name: '_CollectController.isSearching', context: context); + + @override + bool get isSearching { + _$isSearchingAtom.reportRead(); + return super.isSearching; + } + + @override + set isSearching(bool value) { + _$isSearchingAtom.reportWrite(value, super.isSearching, () { + super.isSearching = value; + }); + } + late final _$addCollectAsyncAction = AsyncAction('_CollectController.addCollect', context: context); @@ -63,7 +79,8 @@ mixin _$CollectController on _CollectController, Store { String toString() { return ''' collectibles: ${collectibles}, -searchText: ${searchText} +searchText: ${searchText}, +isSearching: ${isSearching} '''; } } diff --git a/lib/pages/collect/collect_page.dart b/lib/pages/collect/collect_page.dart index 6cbf78935..4adbbcc42 100644 --- a/lib/pages/collect/collect_page.dart +++ b/lib/pages/collect/collect_page.dart @@ -31,6 +31,7 @@ class _CollectPageState extends State bool showDelete = false; bool syncCollectiblesing = false; bool searchBarHovering = false; + late final FocusNode _searchEntryFocusNode = FocusNode(); Future _syncBangumiWithProgress({ required GlobalKey<_FullSyncProgressDialogState> progressDialogKey, @@ -196,6 +197,16 @@ class _CollectPageState extends State ), title: const Text('追番'), actions: [ + AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + transitionBuilder: (Widget child, Animation animation) { + return ScaleTransition(scale: animation, child: child); + }, + child: Observer(builder: (context) => Padding( + padding: const EdgeInsets.only(right: 8.0), + child: searchEntry + )), + ), IconButton( onPressed: () { setState(() { @@ -251,58 +262,9 @@ class _CollectPageState extends State width: 32, height: 32, child: CircularProgressIndicator()) : const Icon(Icons.sync_rounded), ), - body: Stack( - children: [ - Observer(builder: (context) { + body: Observer(builder: (context) { return renderBody; }), - Positioned( - left: 0, - right:0, - bottom: 16, - child: Align( - alignment: Alignment.bottomCenter, - child: FractionallySizedBox( - widthFactor: 0.5, - child: MouseRegion( - onHover: (_) => setState(() => searchBarHovering = true), - onExit: (_) => setState(() => searchBarHovering = false), - child: AnimatedOpacity( - duration: const Duration(milliseconds: 200), - opacity: searchBarHovering ? 1.0 : 0.5, - child: Material( - elevation: 8, - borderRadius: BorderRadius.circular(24), - child: SearchBar( - controller: searchController, - hintText: '搜索番剧', - leading: ValueListenableBuilder( - valueListenable: searchController, - builder: (context, value, child) { - if (value.text.isEmpty) { - return const Icon(Icons.search); - } - return IconButton( - icon: const Icon(Icons.close), - onPressed: () { - searchController.clear(); - collectController.searchText = ''; - }, - ); - }, - ), - onChanged: (text) { - collectController.searchText = text; - }, - ), - ), - ), - ), - ), - ), - ), - ], - ), ), ); } @@ -411,6 +373,102 @@ class _CollectPageState extends State } return gridViewList; } + + Widget get searchEntry { + if (collectController.isSearching) { + return TapRegion( + groupId: 'searchEntryTapRegion', + child: Observer(builder:(context) { + return SizedBox( + height: 48, + width: MediaQuery.sizeOf(context).width * 0.6, + child: SearchBar( + autoFocus: true, + focusNode: _searchEntryFocusNode, + controller: searchController, + hintText: '在收藏中搜索番剧喵~', + hintStyle: WidgetStateProperty.all( + Theme.of(context).textTheme.bodyLarge?.copyWith( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), + ), + elevation: WidgetStateProperty.all(2.0), + shadowColor: WidgetStateProperty.all( + Theme.of(context).colorScheme.shadow, + ), + surfaceTintColor: WidgetStateProperty.all( + Theme.of(context).colorScheme.surfaceTint, + ), + padding: WidgetStateProperty.all( + const EdgeInsets.symmetric(horizontal: 4.0), + ), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28.0), + ), + ), + leading: const Padding( + padding: EdgeInsets.only(left: 8.0), + child: Icon(Icons.search_rounded), + ), + trailing: [ + Observer(builder: (context) => searchEntryActionButton), + ], + onChanged: (value) { + collectController.searchText = value; + }, + )); + }), + onTapOutside: (_) { + collectController.isSearching = false; + }, + ); + } else { + IconButton searchButton = IconButton( + onPressed: () { + collectController.isSearching = true; + }, + icon: const Icon(Icons.search_rounded), + ); + + if (collectController.searchText.isNotEmpty) { + return Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Row( + children: [ + Text( + '搜索: ${collectController.searchText}', + style: Theme.of(context).textTheme.bodyMedium, + ), + searchButton, + ], + ), + ); + } else { + return searchButton; + } + } + } + + Widget get searchEntryActionButton { + if (collectController.searchText.isNotEmpty) { + return IconButton( + padding: const EdgeInsets.all(0), + alignment: Alignment.centerRight, + style: ButtonStyle( + overlayColor: WidgetStateProperty.all(Colors.transparent), + ), + onPressed: () { + searchController.clear(); + collectController.searchText = ''; + _searchEntryFocusNode.requestFocus(); + }, + icon: const Icon(Icons.close_rounded), + ); + } else { + return const SizedBox.shrink(); + } + } } class _FullSyncProgressDialog extends StatefulWidget {