mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 15:57:25 +00:00
search page
This commit is contained in:
@@ -233,88 +233,6 @@ class _MySliverAppBarDelegate extends SliverPersistentHeaderDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
class FloatingSearchBar extends StatefulWidget {
|
||||
const FloatingSearchBar({
|
||||
super.key,
|
||||
this.height = 56,
|
||||
this.trailing,
|
||||
required this.onSearch,
|
||||
required this.controller,
|
||||
this.onChanged,
|
||||
});
|
||||
|
||||
/// height of search bar
|
||||
final double height;
|
||||
|
||||
/// end of search bar
|
||||
final Widget? trailing;
|
||||
|
||||
/// callback when user do search
|
||||
final void Function(String) onSearch;
|
||||
|
||||
/// controller of [TextField]
|
||||
final TextEditingController controller;
|
||||
|
||||
final void Function(String)? onChanged;
|
||||
|
||||
@override
|
||||
State<FloatingSearchBar> createState() => _FloatingSearchBarState();
|
||||
}
|
||||
|
||||
class _FloatingSearchBarState extends State<FloatingSearchBar> {
|
||||
double get effectiveHeight {
|
||||
return math.max(widget.height, 53);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ColorScheme colorScheme = Theme.of(context).colorScheme;
|
||||
var text = widget.controller.text;
|
||||
if (text.isEmpty) {
|
||||
text = "Search";
|
||||
}
|
||||
var padding = 12.0;
|
||||
return Container(
|
||||
padding: EdgeInsets.fromLTRB(padding, 9, padding, 0),
|
||||
width: double.infinity,
|
||||
height: effectiveHeight,
|
||||
child: Material(
|
||||
elevation: 0,
|
||||
color: colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(effectiveHeight / 2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(children: [
|
||||
Tooltip(
|
||||
message: "返回".tl,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: TextField(
|
||||
controller: widget.controller,
|
||||
decoration: const InputDecoration(
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onSubmitted: (s) {
|
||||
widget.onSearch(s);
|
||||
},
|
||||
onChanged: widget.onChanged,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.trailing != null) widget.trailing!
|
||||
]),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FilledTabBar extends StatefulWidget {
|
||||
const FilledTabBar({super.key, this.controller, required this.tabs});
|
||||
|
||||
@@ -420,21 +338,18 @@ class _FilledTabBarState extends State<FilledTabBar> {
|
||||
},
|
||||
);
|
||||
return Container(
|
||||
key: tabBarKey,
|
||||
height: _kTabHeight,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: context.colorScheme.outlineVariant,
|
||||
width: 0.6,
|
||||
key: tabBarKey,
|
||||
height: _kTabHeight,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: context.colorScheme.outlineVariant,
|
||||
width: 0.6,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: widget.tabs.isEmpty
|
||||
? const SizedBox()
|
||||
: child
|
||||
);
|
||||
child: widget.tabs.isEmpty ? const SizedBox() : child);
|
||||
}
|
||||
|
||||
int? previousIndex;
|
||||
@@ -482,11 +397,11 @@ class _FilledTabBarState extends State<FilledTabBar> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: DefaultTextStyle(
|
||||
style: DefaultTextStyle.of(context).style.copyWith(
|
||||
color: i == _controller.index
|
||||
? context.colorScheme.primary
|
||||
: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
color: i == _controller.index
|
||||
? context.colorScheme.primary
|
||||
: context.colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
child: widget.tabs[i],
|
||||
),
|
||||
),
|
||||
@@ -611,8 +526,8 @@ class _IndicatorPainter extends CustomPainter {
|
||||
final Rect toRect = indicatorRect(size, to);
|
||||
_currentRect = Rect.lerp(fromRect, toRect, (value - from).abs());
|
||||
final Paint paint = Paint()..color = color;
|
||||
final RRect rrect =
|
||||
RRect.fromRectAndCorners(_currentRect!, topLeft: Radius.circular(radius), topRight: Radius.circular(radius));
|
||||
final RRect rrect = RRect.fromRectAndCorners(_currentRect!,
|
||||
topLeft: Radius.circular(radius), topRight: Radius.circular(radius));
|
||||
canvas.drawRRect(rrect, paint);
|
||||
}
|
||||
|
||||
@@ -621,3 +536,239 @@ class _IndicatorPainter extends CustomPainter {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class SearchBarController {
|
||||
_SearchBarMixin? _state;
|
||||
|
||||
final void Function(String text)? onSearch;
|
||||
|
||||
final String initialText;
|
||||
|
||||
void setText(String text) {
|
||||
_state?.setText(text);
|
||||
}
|
||||
|
||||
String get text => _state?.getText() ?? '';
|
||||
|
||||
SearchBarController({this.onSearch, this.initialText = ''});
|
||||
}
|
||||
|
||||
abstract mixin class _SearchBarMixin {
|
||||
void setText(String text);
|
||||
|
||||
String getText();
|
||||
}
|
||||
|
||||
class SliverSearchBar extends StatefulWidget {
|
||||
const SliverSearchBar({super.key, required this.controller});
|
||||
|
||||
final SearchBarController controller;
|
||||
|
||||
@override
|
||||
State<SliverSearchBar> createState() => _SliverSearchBarState();
|
||||
}
|
||||
|
||||
class _SliverSearchBarState extends State<SliverSearchBar>
|
||||
with _SearchBarMixin {
|
||||
late TextEditingController _editingController;
|
||||
|
||||
late SearchBarController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_controller = widget.controller;
|
||||
_controller._state = this;
|
||||
_editingController = TextEditingController(text: _controller.initialText);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void setText(String text) {
|
||||
_editingController.text = text;
|
||||
}
|
||||
|
||||
@override
|
||||
String getText() {
|
||||
return _editingController.text;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverPersistentHeader(
|
||||
delegate: _SliverSearchBarDelegate(
|
||||
editingController: _editingController,
|
||||
controller: _controller,
|
||||
topPadding: MediaQuery.of(context).padding.top,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SliverSearchBarDelegate extends SliverPersistentHeaderDelegate {
|
||||
final TextEditingController editingController;
|
||||
|
||||
final SearchBarController controller;
|
||||
|
||||
final double topPadding;
|
||||
|
||||
const _SliverSearchBarDelegate({
|
||||
required this.editingController,
|
||||
required this.controller,
|
||||
required this.topPadding,
|
||||
});
|
||||
|
||||
static const _kAppBarHeight = 52.0;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
BuildContext context, double shrinkOffset, bool overlapsContent) {
|
||||
return Container(
|
||||
height: _kAppBarHeight + topPadding,
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.only(top: topPadding),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
const BackButton(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: TextField(
|
||||
controller: editingController,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search".tl,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onSubmitted: (text) {
|
||||
controller.onSearch?.call(text);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
ListenableBuilder(
|
||||
listenable: editingController,
|
||||
builder: (context, child) {
|
||||
return editingController.text.isEmpty
|
||||
? const SizedBox()
|
||||
: IconButton(
|
||||
iconSize: 20,
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
editingController.clear();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
double get maxExtent => _kAppBarHeight + topPadding;
|
||||
|
||||
@override
|
||||
double get minExtent => _kAppBarHeight + topPadding;
|
||||
|
||||
@override
|
||||
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) {
|
||||
return oldDelegate is! _SliverSearchBarDelegate ||
|
||||
editingController != oldDelegate.editingController ||
|
||||
controller != oldDelegate.controller ||
|
||||
topPadding != oldDelegate.topPadding;
|
||||
}
|
||||
}
|
||||
|
||||
class AppSearchBar extends StatefulWidget {
|
||||
const AppSearchBar({super.key, required this.controller});
|
||||
|
||||
final SearchBarController controller;
|
||||
|
||||
@override
|
||||
State<AppSearchBar> createState() => _SearchBarState();
|
||||
}
|
||||
|
||||
class _SearchBarState extends State<AppSearchBar> with _SearchBarMixin {
|
||||
late TextEditingController _editingController;
|
||||
|
||||
late SearchBarController _controller;
|
||||
|
||||
@override
|
||||
void setText(String text) {
|
||||
_editingController.text = text;
|
||||
}
|
||||
|
||||
@override
|
||||
String getText() {
|
||||
return _editingController.text;
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_controller = widget.controller;
|
||||
_controller._state = this;
|
||||
_editingController = TextEditingController(text: _controller.initialText);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final topPadding = MediaQuery.of(context).padding.top;
|
||||
return Container(
|
||||
height: _kAppBarHeight + topPadding,
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.only(top: topPadding),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const SizedBox(width: 8),
|
||||
const BackButton(),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: TextField(
|
||||
controller: _editingController,
|
||||
decoration: InputDecoration(
|
||||
hintText: "Search".tl,
|
||||
border: InputBorder.none,
|
||||
),
|
||||
onSubmitted: (text) {
|
||||
_controller.onSearch?.call(text);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
ListenableBuilder(
|
||||
listenable: _editingController,
|
||||
builder: (context, child) {
|
||||
return _editingController.text.isEmpty
|
||||
? const SizedBox()
|
||||
: IconButton(
|
||||
iconSize: 20,
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_editingController.clear();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -505,6 +505,7 @@ class ComicList extends StatefulWidget {
|
||||
this.loadNext,
|
||||
this.leadingSliver,
|
||||
this.trailingSliver,
|
||||
this.errorLeading,
|
||||
});
|
||||
|
||||
final Future<Res<List<Comic>>> Function(int page)? loadPage;
|
||||
@@ -515,6 +516,8 @@ class ComicList extends StatefulWidget {
|
||||
|
||||
final Widget? trailingSliver;
|
||||
|
||||
final Widget? errorLeading;
|
||||
|
||||
@override
|
||||
State<ComicList> createState() => _ComicListState();
|
||||
}
|
||||
@@ -691,6 +694,7 @@ class _ComicListState extends State<ComicList> {
|
||||
if (error != null) {
|
||||
return Column(
|
||||
children: [
|
||||
if (widget.errorLeading != null) widget.errorLeading!,
|
||||
buildPageSelector(),
|
||||
Expanded(
|
||||
child: NetworkError(
|
||||
@@ -717,7 +721,8 @@ class _ComicListState extends State<ComicList> {
|
||||
if (widget.leadingSliver != null) widget.leadingSliver!,
|
||||
buildSliverPageSelector(),
|
||||
SliverGridComics(comics: data[page] ?? const []),
|
||||
buildSliverPageSelector(),
|
||||
if(data[page]!.length > 6)
|
||||
buildSliverPageSelector(),
|
||||
if (widget.trailingSliver != null) widget.trailingSliver!,
|
||||
],
|
||||
);
|
||||
|
Reference in New Issue
Block a user