Files
venera/lib/pages/search_page.dart
2024-10-18 11:34:31 +08:00

447 lines
13 KiB
Dart

import 'package:flutter/material.dart';
import 'package:venera/components/components.dart';
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/search_result_page.dart';
import 'package:venera/utils/app_links.dart';
import 'package:venera/utils/ext.dart';
import 'package:venera/utils/tags_translation.dart';
import 'package:venera/utils/translations.dart';
import 'comic_page.dart';
class SearchPage extends StatefulWidget {
const SearchPage({super.key});
@override
State<SearchPage> createState() => _SearchPageState();
}
class _SearchPageState extends State<SearchPage> {
late final SearchBarController controller;
String searchTarget = "";
var options = <String>[];
void update() {
setState(() {});
}
void search([String? text]) {
context
.to(
() => SearchResultPage(
text: text ?? controller.text,
sourceKey: searchTarget,
options: options,
),
)
.then((_) => update());
}
var suggestions = <Pair<String, TranslationType>>[];
bool canHandleUrl(String text) {
if (!text.isURL) return false;
for (var source in ComicSource.all()) {
if (source.linkHandler != null) {
var uri = Uri.parse(text);
if (source.linkHandler!.domains.contains(uri.host)) {
return true;
}
}
}
return false;
}
void findSuggestions() {
var text = controller.text.split(" ").last;
var suggestions = this.suggestions;
suggestions.clear();
if (canHandleUrl(controller.text)) {
suggestions.add(Pair("**URL**", TranslationType.other));
} else {
var text = controller.text;
for (var comicSource in ComicSource.all()) {
if (comicSource.idMatcher?.hasMatch(text) ?? false) {
suggestions.add(Pair(
"**${comicSource.key}**",
TranslationType.other,
));
}
}
}
if (!ComicSource.find(searchTarget)!.enableTagsSuggestions) {
update();
return;
}
bool check(String text, String key, String value) {
if (text.removeAllBlank == "") {
return false;
}
if (key.length >= text.length && key.substring(0, text.length) == text ||
(key.contains(" ") &&
key.split(" ").last.length >= text.length &&
key.split(" ").last.substring(0, text.length) == text)) {
return true;
} else if (value.length >= text.length && value.contains(text)) {
return true;
}
return false;
}
void find(Map<String, String> map, TranslationType type) {
for (var element in map.entries) {
if (suggestions.length > 100) {
break;
}
if (check(text, element.key, element.value)) {
suggestions.add(Pair(element.key, type));
}
}
}
find(TagsTranslation.femaleTags, TranslationType.female);
find(TagsTranslation.maleTags, TranslationType.male);
find(TagsTranslation.parodyTags, TranslationType.parody);
find(TagsTranslation.characterTranslations, TranslationType.character);
find(TagsTranslation.otherTags, TranslationType.other);
find(TagsTranslation.mixedTags, TranslationType.mixed);
find(TagsTranslation.languageTranslations, TranslationType.language);
find(TagsTranslation.artistTags, TranslationType.artist);
find(TagsTranslation.groupTags, TranslationType.group);
find(TagsTranslation.cosplayerTags, TranslationType.cosplayer);
update();
}
@override
void initState() {
var defaultSearchTarget = appdata.settings['defaultSearchTarget'];
if (defaultSearchTarget != null &&
ComicSource.find(defaultSearchTarget) != null) {
searchTarget = defaultSearchTarget;
} else {
searchTarget = ComicSource.all().first.key;
}
controller = SearchBarController(
onSearch: search,
);
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: SmoothCustomScrollView(
slivers: buildSlivers().toList(),
),
);
}
Iterable<Widget> buildSlivers() sync* {
yield SliverSearchBar(
controller: controller,
onChanged: (s) {
findSuggestions();
},
);
if (suggestions.isNotEmpty) {
yield buildSuggestions(context);
} else {
yield buildSearchTarget();
yield buildSearchOptions();
yield buildSearchHistory();
}
}
Widget buildSearchTarget() {
var sources =
ComicSource.all().where((e) => e.searchPageData != null).toList();
return SliverToBoxAdapter(
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
ListTile(
contentPadding: EdgeInsets.zero,
title: Text("Search in".tl),
),
Wrap(
spacing: 8,
runSpacing: 8,
children: sources.map((e) {
return OptionChip(
text: e.name.tl,
isSelected: searchTarget == e.key,
onTap: () {
setState(() {
searchTarget = e.key;
});
},
);
}).toList(),
),
],
),
),
);
}
Widget buildSearchOptions() {
var children = <Widget>[];
final searchOptions =
ComicSource.find(searchTarget)!.searchPageData!.searchOptions ??
<SearchOptions>[];
if (searchOptions.length != options.length) {
options = searchOptions.map((e) => e.defaultValue).toList();
}
if (searchOptions.isEmpty) {
return const SliverToBoxAdapter(child: SizedBox());
}
for (int i = 0; i < searchOptions.length; i++) {
final option = searchOptions[i];
children.add(ListTile(
contentPadding: EdgeInsets.zero,
title: Text(option.label.tl),
));
children.add(Wrap(
runSpacing: 8,
spacing: 8,
children: option.options.entries.map((e) {
return OptionChip(
text: e.value.ts(searchTarget),
isSelected: options[i] == e.key,
onTap: () {
options[i] = e.key;
update();
},
);
}).toList(),
));
}
return SliverToBoxAdapter(
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
),
),
);
}
Widget buildSearchHistory() {
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
if (index == 0) {
return const Divider(
thickness: 0.6,
).paddingTop(16);
}
if (index == 1) {
return ListTile(
leading: const Icon(Icons.history),
contentPadding: EdgeInsets.zero,
title: Text("Search History".tl),
trailing: Flyout(
flyoutBuilder: (context) {
return FlyoutContent(
title: "Clear Search History".tl,
actions: [
FilledButton(
child: Text("Clear".tl),
onPressed: () {
appdata.clearSearchHistory();
context.pop();
update();
},
)
],
);
},
child: Builder(
builder: (context) {
return Tooltip(
message: "Clear".tl,
child: IconButton(
icon: const Icon(Icons.clear_all),
onPressed: () {
context
.findAncestorStateOfType<FlyoutState>()!
.show();
},
),
);
},
),
),
);
}
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 12),
title: Text(appdata.searchHistory[index - 2]),
onTap: () {
search(appdata.searchHistory[index - 2]);
},
);
},
childCount: 2 + appdata.searchHistory.length,
),
).sliverPaddingHorizontal(16);
}
Widget buildSuggestions(BuildContext context) {
bool check(String text, String key, String value) {
if (text.removeAllBlank == "") {
return false;
}
if (key.length >= text.length && key.substring(0, text.length) == text ||
(key.contains(" ") &&
key.split(" ").last.length >= text.length &&
key.split(" ").last.substring(0, text.length) == text)) {
return true;
} else if (value.length >= text.length && value.contains(text)) {
return true;
}
return false;
}
void onSelected(String text, TranslationType? type) {
var words = controller.text.split(" ");
if (words.length >= 2 &&
check("${words[words.length - 2]} ${words[words.length - 1]}", text,
text.translateTagsToCN)) {
controller.text = controller.text.replaceLast(
"${words[words.length - 2]} ${words[words.length - 1]}", "");
} else {
controller.text =
controller.text.replaceLast(words[words.length - 1], "");
}
if (type != null) {
controller.text += "${type.name}:$text ";
} else {
controller.text += "$text ";
}
update();
}
bool showMethod = MediaQuery.of(context).size.width < 600;
bool showTranslation = App.locale.languageCode == "zh";
Widget buildItem(Pair<String, TranslationType> value) {
if (value.left == "**URL**") {
return ListTile(
leading: const Icon(Icons.link),
title: Text("Open link".tl),
subtitle: Text(
controller.text,
maxLines: 1,
overflow: TextOverflow.fade,
),
trailing: const Icon(Icons.arrow_right),
onTap: () {
handleAppLink(Uri.parse(controller.text));
},
);
}
if (RegExp(r"^\*\*.*\*\*$").hasMatch(value.left)) {
var key = value.left.substring(2, value.left.length - 2);
var comicSource = ComicSource.find(key);
if (comicSource == null) {
return const SizedBox();
}
return ListTile(
leading: const Icon(Icons.link),
title: Text("${"Open comic".tl}: ${comicSource.name}"),
subtitle: Text(
controller.text,
maxLines: 1,
overflow: TextOverflow.fade,
),
trailing: const Icon(Icons.arrow_right),
onTap: () {
context.to(
() => ComicPage(
sourceKey: key,
id: controller.text,
),
);
},
);
}
var subTitle = TagsTranslation.translationTagWithNamespace(
value.left, value.right.name);
return ListTile(
title: Row(
mainAxisSize: MainAxisSize.min,
children: [
Expanded(
child: Text(value.left),
),
if (!showMethod)
const SizedBox(
width: 12,
),
if (!showMethod && showTranslation)
Text(
subTitle,
style: TextStyle(
fontSize: 14, color: Theme.of(context).colorScheme.outline),
)
],
),
subtitle: (showMethod && showTranslation) ? Text(subTitle) : null,
trailing: Text(
value.right.name,
style: const TextStyle(fontSize: 13),
),
onTap: () => onSelected(value.left, value.right),
);
}
return SliverMainAxisGroup(
slivers: [
SliverToBoxAdapter(
child: ListTile(
leading: const Icon(Icons.hub_outlined),
title: Text("Suggestions".tl),
trailing: Tooltip(
message: "Clear".tl,
child: IconButton(
icon: const Icon(Icons.clear_all),
onPressed: () {
suggestions.clear();
update();
},
),
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) {
return buildItem(suggestions[index]);
},
childCount: suggestions.length,
),
),
],
);
}
}