diff --git a/lib/basic/methods.dart b/lib/basic/methods.dart index 78b039e..dcfe0fa 100644 --- a/lib/basic/methods.dart +++ b/lib/basic/methods.dart @@ -39,6 +39,15 @@ class Methods { return ComicsResponse.fromJson(jsonDecode(rsp)); } + Future comicSearch(String searchQuery, SortBy sortBy, int page) async { + final rsp = await _invoke("comic_search", { + "search_query": searchQuery, + "sort_by": sortBy.value, + "page": page, + }); + return ComicsResponse.fromJson(jsonDecode(rsp)); + } + Future categories() async { return CategoriesResponse.fromJson( jsonDecode(await _invoke("categories", ""))); diff --git a/lib/screens/app_screen.dart b/lib/screens/app_screen.dart index 5c204f3..1342450 100644 --- a/lib/screens/app_screen.dart +++ b/lib/screens/app_screen.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; import 'package:jasmine/screens/browser_screen.dart'; +import 'package:jasmine/screens/comic_search_screen.dart'; import 'package:jasmine/screens/components/badge.dart'; +import 'package:jasmine/screens/components/floating_search_bar.dart'; import 'package:jasmine/screens/user_screen.dart'; +import 'components/comic_floating_search_bar.dart'; + class AppScreen extends StatefulWidget { const AppScreen({Key? key}) : super(key: key); @@ -10,7 +14,24 @@ class AppScreen extends StatefulWidget { State createState() => _AppScreenState(); } -class _AppScreenState extends State { +class _AppScreenState extends State { + final _searchBarController = FloatingSearchBarController(); + + late final List _screens = [ + AppScreenData( + BrowserScreen(searchBarController: _searchBarController), + '浏览', + const Icon(Icons.menu_book_outlined), + const Icon(Icons.menu_book), + ), + const AppScreenData( + UserScreen(), + '书架', + VersionBadged(child: Icon(Icons.image_outlined)), + VersionBadged(child: Icon(Icons.image)), + ), + ]; + @override void dispose() { _pageController.dispose(); @@ -31,52 +52,45 @@ class _AppScreenState extends State { @override Widget build(BuildContext context) { - return Scaffold( - body: PageView( - allowImplicitScrolling: false, - controller: _pageController, - onPageChanged: (index) { - setState(() { - _selectedIndex = index; - }); - }, - children: _screens.map((e) => e.screen).toList(), - ), - bottomNavigationBar: BottomNavigationBar( - items: _screens - .map((e) => BottomNavigationBarItem( - label: e.title, - icon: e.icon, - activeIcon: e.activeIcon, - )) - .toList(), - currentIndex: _selectedIndex, - iconSize: 20, - selectedFontSize: 12, - unselectedFontSize: 12, - onTap: _onItemTapped, - selectedItemColor: Colors.black, - unselectedItemColor: Colors.black.withAlpha(120), + return ComicFloatingSearchBarScreen( + onQuery: (value) { + Navigator.of(context).push(MaterialPageRoute(builder: (_) { + return ComicSearchScreen(initKeywords: value); + })); + }, + controller: _searchBarController, + child: Scaffold( + body: PageView( + allowImplicitScrolling: false, + controller: _pageController, + onPageChanged: (index) { + setState(() { + _selectedIndex = index; + }); + }, + children: _screens.map((e) => e.screen).toList(), + ), + bottomNavigationBar: BottomNavigationBar( + items: _screens + .map((e) => BottomNavigationBarItem( + label: e.title, + icon: e.icon, + activeIcon: e.activeIcon, + )) + .toList(), + currentIndex: _selectedIndex, + iconSize: 20, + selectedFontSize: 12, + unselectedFontSize: 12, + onTap: _onItemTapped, + selectedItemColor: Colors.black, + unselectedItemColor: Colors.black.withAlpha(120), + ), ), ); } } -const List _screens = [ - AppScreenData( - BrowserScreen(), - '浏览', - Icon(Icons.menu_book_outlined), - Icon(Icons.menu_book), - ), - AppScreenData( - UserScreen(), - '书架', - VersionBadged(child: Icon(Icons.image_outlined)), - VersionBadged(child: Icon(Icons.image)), - ), -]; - class AppScreenData { final Widget screen; final String title; diff --git a/lib/screens/browser_screen.dart b/lib/screens/browser_screen.dart index 520d157..73d5ed1 100644 --- a/lib/screens/browser_screen.dart +++ b/lib/screens/browser_screen.dart @@ -5,11 +5,16 @@ import 'package:jasmine/basic/commons.dart'; import 'package:jasmine/basic/methods.dart'; import 'package:jasmine/screens/components/comic_pager.dart'; import 'package:jasmine/screens/components/content_builder.dart'; +import 'package:jasmine/screens/components/floating_search_bar.dart'; import 'components/browser_bottom_sheet.dart'; +import 'components/actions.dart'; class BrowserScreen extends StatefulWidget { - const BrowserScreen({Key? key}) : super(key: key); + final FloatingSearchBarController searchBarController; + + const BrowserScreen({Key? key, required this.searchBarController}) + : super(key: key); @override State createState() => _BrowserScreenState(); @@ -41,14 +46,21 @@ class _BrowserScreenState extends State _future = methods.categories(); }); }, - successBuilder: - (BuildContext context, AsyncSnapshot snapshot) { + successBuilder: ( + BuildContext context, + AsyncSnapshot snapshot, + ) { final categories = snapshot.requireData.categories; return Scaffold( appBar: AppBar( title: const Text("浏览"), actions: [ - IconButton(onPressed: () {}, icon: const Icon(Icons.search)), + IconButton( + onPressed: () { + widget.searchBarController.display(modifyInput: ""); + }, + icon: const Icon(Icons.search), + ), const BrowserBottomSheetAction(), ], ), @@ -74,7 +86,11 @@ class _BrowserScreenState extends State }, ), ), - _buildOrderSwitch(), + buildOrderSwitch(context,_sortBy,(value){ + setState(() { + _sortBy = value; + }); + }), ], ), ), @@ -95,31 +111,6 @@ class _BrowserScreenState extends State ); } - Widget _buildOrderSwitch() { - final iconColor = Theme.of(context).appBarTheme.iconTheme?.color; - return MaterialButton( - onPressed: () async { - final target = await chooseSortBy(context); - if (target != null) { - setState(() { - _sortBy = target; - }); - } - }, - child: Column( - children: [ - Expanded(child: Container()), - Icon( - Icons.sort, - color: iconColor, - ), - Expanded(child: Container()), - Text(_sortBy.toString(), style: TextStyle(color: iconColor)), - Expanded(child: Container()), - ], - ), - ); - } } class _MTabBar extends StatefulWidget { diff --git a/lib/screens/comic_search_screen.dart b/lib/screens/comic_search_screen.dart new file mode 100644 index 0000000..18f579e --- /dev/null +++ b/lib/screens/comic_search_screen.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; +import 'package:jasmine/basic/methods.dart'; +import 'package:jasmine/screens/components/floating_search_bar.dart'; + +import 'components/browser_bottom_sheet.dart'; +import 'components/comic_floating_search_bar.dart'; +import 'components/comic_pager.dart'; +import 'components/actions.dart'; + +class ComicSearchScreen extends StatefulWidget { + final String initKeywords; + + const ComicSearchScreen({required this.initKeywords, Key? key}) + : super(key: key); + + @override + State createState() => _ComicSearchScreenState(); +} + +class _ComicSearchScreenState extends State { + final _controller = FloatingSearchBarController(); + late var _keywords = widget.initKeywords; + SortBy _sortBy = sortByDefault; + + @override + Widget build(BuildContext context) { + return ComicFloatingSearchBarScreen( + controller: _controller, + onQuery: (value) { + setState(() { + _keywords = value; + }); + }, + child: Scaffold( + appBar: AppBar( + title: Text(_keywords), + actions: [ + IconButton( + onPressed: () { + _controller.display(modifyInput: _keywords); + }, + icon: const Icon(Icons.search), + ), + const BrowserBottomSheetAction(), + buildOrderSwitch(context, _sortBy, (value) { + setState(() { + _sortBy = value; + }); + }), + ], + ), + body: ComicPager( + key: Key("$_keywords:$_sortBy"), + onPage: (int page) async { + final response = await methods.comicSearch( + _keywords, + _sortBy, + page, + ); + return response; + }, + ), + ), + ); + } +} diff --git a/lib/screens/components/actions.dart b/lib/screens/components/actions.dart new file mode 100644 index 0000000..4225bfe --- /dev/null +++ b/lib/screens/components/actions.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import 'package:jasmine/basic/commons.dart'; +import 'package:jasmine/basic/entities.dart'; + +Widget buildOrderSwitch( + BuildContext context, + SortBy value, + ValueChanged valueChanged, +) { + final iconColor = Theme.of(context).appBarTheme.iconTheme?.color; + return MaterialButton( + onPressed: () async { + final target = await chooseSortBy(context); + if (target != null) { + valueChanged(target); + } + }, + child: Column( + children: [ + Expanded(child: Container()), + Icon( + Icons.sort, + color: iconColor, + ), + Expanded(child: Container()), + Text(value.toString(), style: TextStyle(color: iconColor)), + Expanded(child: Container()), + ], + ), + ); +} diff --git a/lib/screens/components/comic_floating_search_bar.dart b/lib/screens/components/comic_floating_search_bar.dart new file mode 100644 index 0000000..db1e0e1 --- /dev/null +++ b/lib/screens/components/comic_floating_search_bar.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; + +import '../comic_search_screen.dart'; +import 'floating_search_bar.dart'; + +class ComicFloatingSearchBarScreen extends StatefulWidget { + final FloatingSearchBarController controller; + final Widget child; + final ValueChanged? onQuery; + + const ComicFloatingSearchBarScreen({ + Key? key, + required this.controller, + required this.child, + this.onQuery, + }) : super(key: key); + + @override + State createState() => _ComicFloatingSearchBarScreenState(); +} + +class _ComicFloatingSearchBarScreenState + extends State { + @override + Widget build(BuildContext context) { + return FloatingSearchBarScreen( + controller: widget.controller, + child: widget.child, + onSubmitted: _onSubmitted, + ); + } + + void _onSubmitted(String value) { + widget.controller.hide(); + if (value.isNotEmpty && widget.onQuery != null) { + widget.onQuery!(value); + } + } +} diff --git a/lib/screens/components/floating_search_bar.dart b/lib/screens/components/floating_search_bar.dart new file mode 100644 index 0000000..5602fda --- /dev/null +++ b/lib/screens/components/floating_search_bar.dart @@ -0,0 +1,217 @@ +import 'package:flutter/material.dart'; + +class FloatingSearchBarScreen extends StatefulWidget { + final FloatingSearchBarController controller; + final Widget child; + final ValueChanged? onSubmitted; + final String? hint; + final bool showCursor; + final bool autocorrect; + + const FloatingSearchBarScreen({ + required this.controller, + required this.child, + this.hint, + this.showCursor = true, + this.autocorrect = true, + this.onSubmitted, + Key? key, + }) : super(key: key); + + @override + State createState() => _FloatingSearchBarScreenState(); +} + +class _FloatingSearchBarScreenState extends State + with SingleTickerProviderStateMixin { + final _node = FocusNode(); + late final TextEditingController _textEditingController = + TextEditingController(); + late final AnimationController _animationController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 300), + ); + late final _in = Tween(begin: 0.0, end: 1.0).animate(_animationController); + + @override + void initState() { + widget.controller._state = this; + super.initState(); + } + + @override + void dispose() { + _node.dispose(); + _textEditingController.dispose(); + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Stack( + children: [ + widget.child, + _buildBackdrop(), + _buildSearchBar(), + _buildOnPop(), + ], + ), + ); + } + + Widget _buildOnPop() { + return WillPopScope( + onWillPop: () async { + if (_animationController.isDismissed) { + return true; + } + _animationController.reverse(); + return false; + }, + child: Container(), + ); + } + + Widget _buildBackdrop() { + return LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return AnimatedBuilder( + animation: _in, + builder: (BuildContext context, Widget? child) { + if (_in.value > 0) { + return GestureDetector( + onTap: () { + _hideSearchBar(); + }, + child: Container( + width: constraints.maxWidth, + height: constraints.maxHeight, + color: Colors.black.withOpacity(.3 * _in.value), + ), + ); + } + return Container(); + }, + ); + }, + ); + } + + Widget _buildSearchBar() { + double statusBarHeight = MediaQuery.of(context).padding.top; + double finalHeight = 80 + statusBarHeight; + return AnimatedBuilder( + animation: _in, + builder: (BuildContext context, Widget? child) { + return Container( + padding: EdgeInsets.only(top: statusBarHeight), + child: Transform.translate( + offset: Offset(0, (_in.value * finalHeight) - finalHeight), + child: Column( + children: [ + _SearchBarContainer( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + IconButton( + onPressed: _hideSearchBar, + icon: Icon( + Icons.arrow_back, + color: Colors.grey.shade800, + ), + ), + Expanded(child: _buildTextField()), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } + + Widget _buildTextField() { + return TextField( + controller: _textEditingController, + showCursor: widget.showCursor, + scrollPadding: EdgeInsets.zero, + scrollPhysics: const NeverScrollableScrollPhysics(), + focusNode: _node, + maxLines: 1, + autofocus: false, + autocorrect: widget.autocorrect, + //cursorColor: style.accentColor, + //style: style.queryStyle, + // textInputAction: widget.textInputAction, + // keyboardType: widget.textInputType, + onSubmitted: widget.onSubmitted, + decoration: InputDecoration( + isDense: true, + hintText: widget.hint ?? "搜索", + // hintStyle: style.hintStyle, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + errorBorder: InputBorder.none, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, + ), + ); + } + + void _displayFloatingSearchBar({String? modifyInput}) { + if (modifyInput != null) { + _textEditingController.text = modifyInput; + } + _node.requestFocus(); + _animationController.forward(); + } + + void _hideSearchBar() { + _animationController.reverse(); + } +} + +class _SearchBarContainer extends StatelessWidget { + final Widget child; + + const _SearchBarContainer({Key? key, required this.child}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + margin: const EdgeInsets.fromLTRB(8, 5, 8, 5), + height: 50, + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey.shade500.withOpacity(.3), + width: .1, + ), + color: Colors.white, + boxShadow: [ + BoxShadow( + blurRadius: .2, + spreadRadius: .3, + color: Colors.grey.shade500.withOpacity(.3), + ), + ], + borderRadius: BorderRadius.circular(5), + ), + child: child, + ); + } +} + +class FloatingSearchBarController { + _FloatingSearchBarScreenState? _state; + + void hide() => _state?._hideSearchBar(); + + void display({String? modifyInput}) => + _state?._displayFloatingSearchBar(modifyInput: modifyInput); +}