mirror of
https://github.com/venera-app/venera.git
synced 2025-09-27 07:47:24 +00:00
aggregated search
This commit is contained in:
213
lib/pages/aggregated_search_page.dart
Normal file
213
lib/pages/aggregated_search_page.dart
Normal file
@@ -0,0 +1,213 @@
|
||||
import "package:flutter/material.dart";
|
||||
import "package:shimmer/shimmer.dart";
|
||||
import "package:venera/components/components.dart";
|
||||
import "package:venera/foundation/app.dart";
|
||||
import "package:venera/foundation/comic_source/comic_source.dart";
|
||||
import "package:venera/foundation/image_provider/cached_image.dart";
|
||||
import "package:venera/pages/search_result_page.dart";
|
||||
|
||||
import "comic_page.dart";
|
||||
|
||||
class AggregatedSearchPage extends StatefulWidget {
|
||||
const AggregatedSearchPage({super.key, required this.keyword});
|
||||
|
||||
final String keyword;
|
||||
|
||||
@override
|
||||
State<AggregatedSearchPage> createState() => _AggregatedSearchPageState();
|
||||
}
|
||||
|
||||
class _AggregatedSearchPageState extends State<AggregatedSearchPage> {
|
||||
late final List<ComicSource> sources;
|
||||
|
||||
late final SearchBarController controller;
|
||||
|
||||
var _keyword = "";
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
sources = ComicSource.all().where((e) => e.searchPageData != null).toList();
|
||||
_keyword = widget.keyword;
|
||||
controller = SearchBarController(
|
||||
currentText: widget.keyword,
|
||||
onSearch: (text) {
|
||||
setState(() {
|
||||
_keyword = text;
|
||||
});
|
||||
},
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SmoothCustomScrollView(slivers: [
|
||||
SliverSearchBar(controller: controller),
|
||||
SliverList(
|
||||
key: ValueKey(_keyword),
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
final source = sources[index];
|
||||
return _SliverSearchResult(source: source, keyword: widget.keyword);
|
||||
},
|
||||
childCount: sources.length,
|
||||
),
|
||||
),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
class _SliverSearchResult extends StatefulWidget {
|
||||
const _SliverSearchResult({required this.source, required this.keyword});
|
||||
|
||||
final ComicSource source;
|
||||
|
||||
final String keyword;
|
||||
|
||||
@override
|
||||
State<_SliverSearchResult> createState() => _SliverSearchResultState();
|
||||
}
|
||||
|
||||
class _SliverSearchResultState extends State<_SliverSearchResult>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
bool isLoading = true;
|
||||
|
||||
static const _kComicHeight = 144.0;
|
||||
|
||||
get _comicWidth => _kComicHeight * 0.72;
|
||||
|
||||
static const _kLeftPadding = 16.0;
|
||||
|
||||
List<Comic>? comics;
|
||||
|
||||
void load() async {
|
||||
final data = widget.source.searchPageData!;
|
||||
var options =
|
||||
(data.searchOptions ?? []).map((e) => e.defaultValue).toList();
|
||||
if (data.loadPage != null) {
|
||||
var res = await data.loadPage!(widget.keyword, 1, options);
|
||||
if (!res.error) {
|
||||
setState(() {
|
||||
comics = res.data;
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
} else if (data.loadNext != null) {
|
||||
var res = await data.loadNext!(widget.keyword, null, options);
|
||||
if (!res.error) {
|
||||
setState(() {
|
||||
comics = res.data;
|
||||
isLoading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
load();
|
||||
}
|
||||
|
||||
Widget buildPlaceHolder() {
|
||||
return Container(
|
||||
height: _kComicHeight,
|
||||
width: _comicWidth,
|
||||
margin: const EdgeInsets.only(left: _kLeftPadding),
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surfaceContainerLow,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildComic(Comic c) {
|
||||
return AnimatedTapRegion(
|
||||
borderRadius: 8,
|
||||
onTap: () {
|
||||
context.to(() => ComicPage(
|
||||
id: c.id,
|
||||
sourceKey: c.sourceKey,
|
||||
));
|
||||
},
|
||||
child: Container(
|
||||
height: _kComicHeight,
|
||||
width: _comicWidth,
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surfaceContainerLow,
|
||||
),
|
||||
child: AnimatedImage(
|
||||
width: _comicWidth,
|
||||
height: _kComicHeight,
|
||||
fit: BoxFit.cover,
|
||||
image: CachedImageProvider(c.cover),
|
||||
),
|
||||
),
|
||||
).paddingLeft(_kLeftPadding);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
context.to(
|
||||
() => SearchResultPage(
|
||||
text: widget.keyword,
|
||||
sourceKey: widget.source.key,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
title: Text(widget.source.name),
|
||||
),
|
||||
if (isLoading)
|
||||
SizedBox(
|
||||
height: _kComicHeight,
|
||||
width: double.infinity,
|
||||
child: Shimmer.fromColors(
|
||||
baseColor: context.colorScheme.surfaceContainerLow,
|
||||
highlightColor: context.colorScheme.surfaceContainer,
|
||||
direction: ShimmerDirection.ltr,
|
||||
child: LayoutBuilder(builder: (context, constrains) {
|
||||
var itemWidth = _comicWidth + _kLeftPadding;
|
||||
var items = (constrains.maxWidth / itemWidth).ceil();
|
||||
return Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Row(
|
||||
children: List.generate(
|
||||
items,
|
||||
(index) => buildPlaceHolder(),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
height: _kComicHeight,
|
||||
child: ListView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
for (var c in comics!) buildComic(c),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
).paddingBottom(16),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
@@ -7,6 +7,7 @@ import 'package:venera/foundation/app.dart';
|
||||
import 'package:venera/foundation/appdata.dart';
|
||||
import 'package:venera/foundation/comic_source/comic_source.dart';
|
||||
import 'package:venera/foundation/state_controller.dart';
|
||||
import 'package:venera/pages/aggregated_search_page.dart';
|
||||
import 'package:venera/pages/search_result_page.dart';
|
||||
import 'package:venera/utils/app_links.dart';
|
||||
import 'package:venera/utils/ext.dart';
|
||||
@@ -27,6 +28,8 @@ class _SearchPageState extends State<SearchPage> {
|
||||
|
||||
String searchTarget = "";
|
||||
|
||||
bool aggregatedSearch = false;
|
||||
|
||||
var focusNode = FocusNode();
|
||||
|
||||
var options = <String>[];
|
||||
@@ -36,15 +39,21 @@ class _SearchPageState extends State<SearchPage> {
|
||||
}
|
||||
|
||||
void search([String? text]) {
|
||||
context
|
||||
.to(
|
||||
() => SearchResultPage(
|
||||
text: text ?? controller.text,
|
||||
sourceKey: searchTarget,
|
||||
options: options,
|
||||
),
|
||||
)
|
||||
.then((_) => update());
|
||||
if (aggregatedSearch) {
|
||||
context
|
||||
.to(() => AggregatedSearchPage(keyword: text ?? controller.text))
|
||||
.then((_) => update());
|
||||
} else {
|
||||
context
|
||||
.to(
|
||||
() => SearchResultPage(
|
||||
text: text ?? controller.text,
|
||||
sourceKey: searchTarget,
|
||||
options: options,
|
||||
),
|
||||
)
|
||||
.then((_) => update());
|
||||
}
|
||||
}
|
||||
|
||||
var suggestions = <Pair<String, TranslationType>>[];
|
||||
@@ -189,6 +198,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
leading: const Icon(Icons.search),
|
||||
title: Text("Search in".tl),
|
||||
),
|
||||
Wrap(
|
||||
@@ -197,8 +207,9 @@ class _SearchPageState extends State<SearchPage> {
|
||||
children: sources.map((e) {
|
||||
return OptionChip(
|
||||
text: e.name,
|
||||
isSelected: searchTarget == e.key,
|
||||
isSelected: searchTarget == e.key || aggregatedSearch,
|
||||
onTap: () {
|
||||
if (aggregatedSearch) return;
|
||||
setState(() {
|
||||
searchTarget = e.key;
|
||||
useDefaultOptions();
|
||||
@@ -207,6 +218,18 @@ class _SearchPageState extends State<SearchPage> {
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text("Aggregated Search".tl),
|
||||
leading: Checkbox(
|
||||
value: aggregatedSearch,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
aggregatedSearch = value ?? false;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -221,6 +244,10 @@ class _SearchPageState extends State<SearchPage> {
|
||||
}
|
||||
|
||||
Widget buildSearchOptions() {
|
||||
if (aggregatedSearch) {
|
||||
return const SliverToBoxAdapter(child: SizedBox());
|
||||
}
|
||||
|
||||
var children = <Widget>[];
|
||||
|
||||
final searchOptions =
|
||||
@@ -262,9 +289,9 @@ class _SearchPageState extends State<SearchPage> {
|
||||
delegate: SliverChildBuilderDelegate(
|
||||
(context, index) {
|
||||
if (index == 0) {
|
||||
return const Divider(
|
||||
thickness: 0.6,
|
||||
).paddingTop(16);
|
||||
return const SizedBox(
|
||||
height: 16,
|
||||
);
|
||||
}
|
||||
if (index == 1) {
|
||||
return ListTile(
|
||||
|
@@ -14,14 +14,14 @@ class SearchResultPage extends StatefulWidget {
|
||||
super.key,
|
||||
required this.text,
|
||||
required this.sourceKey,
|
||||
required this.options,
|
||||
this.options,
|
||||
});
|
||||
|
||||
final String text;
|
||||
|
||||
final String sourceKey;
|
||||
|
||||
final List<String> options;
|
||||
final List<String>? options;
|
||||
|
||||
@override
|
||||
State<SearchResultPage> createState() => _SearchResultPageState();
|
||||
@@ -99,7 +99,7 @@ class _SearchResultPageState extends State<SearchResultPage> {
|
||||
onSearch: search,
|
||||
);
|
||||
sourceKey = widget.sourceKey;
|
||||
options = widget.options;
|
||||
options = widget.options ?? const [];
|
||||
validateOptions();
|
||||
text = widget.text;
|
||||
appdata.addSearchHistory(text);
|
||||
|
Reference in New Issue
Block a user