global search feature

This commit is contained in:
kodjodevf 2023-04-12 14:24:27 +01:00
parent 335fba457a
commit 8bba523ba6
26 changed files with 471 additions and 73 deletions

View file

@ -6,11 +6,13 @@ import 'package:mangayomi/models/manga_type.dart';
import 'package:mangayomi/models/model_manga.dart';
import 'package:mangayomi/views/browse/browse_screen.dart';
import 'package:mangayomi/views/browse/extension/extension_lang.dart';
import 'package:mangayomi/views/browse/global_search_screen.dart';
import 'package:mangayomi/views/general/general_screen.dart';
import 'package:mangayomi/views/history/history_screen.dart';
import 'package:mangayomi/views/library/library_screen.dart';
import 'package:mangayomi/views/manga/detail/manga_reader_detail.dart';
import 'package:mangayomi/views/manga/home/manga_home_screen.dart';
import 'package:mangayomi/views/manga/home/manga_search_screen.dart';
import 'package:mangayomi/views/manga/reader/manga_reader_view.dart';
import 'package:mangayomi/views/more/more_screen.dart';
import 'package:mangayomi/views/more/settings/appearance/appearance_screen.dart';
@ -177,6 +179,44 @@ class AsyncRouterNotifier extends ChangeNotifier {
);
},
),
GoRoute(
path: "/globalSearch",
name: "globalSearch",
builder: (context, state) {
return const GlobalSearchScreen();
},
pageBuilder: (context, state) {
return CustomTransition(
key: state.pageKey,
child: const GlobalSearchScreen(),
);
},
),
GoRoute(
path: "/searchResult",
name: "searchResult",
builder: (context, state) {
final data = state.extra as Map<String, dynamic>;
return SearchResultScreen(
query: data['query']!,
lang: data['lang']!,
source: data['source']!,
viewOnly: data['viewOnly'],
);
},
pageBuilder: (context, state) {
final data = state.extra as Map<String, dynamic>;
return CustomTransition(
key: state.pageKey,
child: SearchResultScreen(
query: data['query']!,
lang: data['lang']!,
source: data['source']!,
viewOnly: data['viewOnly'],
),
);
},
),
];
}

View file

@ -7,7 +7,7 @@ part of 'get_manga_chapter_url.dart';
// **************************************************************************
String _$getMangaChapterUrlHash() =>
r'a9425fc3bdffdf9d8f50acdd7e58671d8ff41a6a';
r'b17d3053901db005a4eee673163d2dc78ceb75f4';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -79,7 +79,7 @@ beautifyChapterName(String? vol, String? chap, String? title, String? lang) {
Future<GetMangaDetailModel> getMangaDetail(GetMangaDetailRef ref,
{required String imageUrl,
required String url,
required String name,
required String title,
required String lang,
required String source}) async {
List<String> genre = [];
@ -604,7 +604,7 @@ Future<GetMangaDetailModel> getMangaDetail(GetMangaDetailRef ref,
genre: genre,
author: author,
description: description,
name: name,
name: title,
url: url,
source: source,
imageUrl: imageUrl,

View file

@ -6,7 +6,7 @@ part of 'get_manga_detail.dart';
// RiverpodGenerator
// **************************************************************************
String _$getMangaDetailHash() => r'9079556aadc0a4b027620d3c5b75280268c022f8';
String _$getMangaDetailHash() => r'b312cc1f35a45520a827c87b7be862cf5f67ffb3';
/// Copied from Dart SDK
class _SystemHash {
@ -44,14 +44,14 @@ class GetMangaDetailFamily extends Family<AsyncValue<GetMangaDetailModel>> {
GetMangaDetailProvider call({
required String imageUrl,
required String url,
required String name,
required String title,
required String lang,
required String source,
}) {
return GetMangaDetailProvider(
imageUrl: imageUrl,
url: url,
name: name,
title: title,
lang: lang,
source: source,
);
@ -64,7 +64,7 @@ class GetMangaDetailFamily extends Family<AsyncValue<GetMangaDetailModel>> {
return call(
imageUrl: provider.imageUrl,
url: provider.url,
name: provider.name,
title: provider.title,
lang: provider.lang,
source: provider.source,
);
@ -92,7 +92,7 @@ class GetMangaDetailProvider
GetMangaDetailProvider({
required this.imageUrl,
required this.url,
required this.name,
required this.title,
required this.lang,
required this.source,
}) : super.internal(
@ -100,7 +100,7 @@ class GetMangaDetailProvider
ref,
imageUrl: imageUrl,
url: url,
name: name,
title: title,
lang: lang,
source: source,
),
@ -117,7 +117,7 @@ class GetMangaDetailProvider
final String imageUrl;
final String url;
final String name;
final String title;
final String lang;
final String source;
@ -126,7 +126,7 @@ class GetMangaDetailProvider
return other is GetMangaDetailProvider &&
other.imageUrl == imageUrl &&
other.url == url &&
other.name == name &&
other.title == title &&
other.lang == lang &&
other.source == source;
}
@ -136,7 +136,7 @@ class GetMangaDetailProvider
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, imageUrl.hashCode);
hash = _SystemHash.combine(hash, url.hashCode);
hash = _SystemHash.combine(hash, name.hashCode);
hash = _SystemHash.combine(hash, title.hashCode);
hash = _SystemHash.combine(hash, lang.hashCode);
hash = _SystemHash.combine(hash, source.hashCode);

View file

@ -1,3 +1,5 @@
// ignore_for_file: unused_local_variable
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;

View file

@ -6,7 +6,7 @@ part of 'get_popular_manga.dart';
// RiverpodGenerator
// **************************************************************************
String _$getPopularMangaHash() => r'57e376026390038dbcf96f93b7a2e25a188d3156';
String _$getPopularMangaHash() => r'cc0567f4659ae10b5e55ccdbb5f5a9560cc09a16';
/// Copied from Dart SDK
class _SystemHash {

View file

@ -92,7 +92,59 @@ Future<SearchMangaModel> searchManga(SearchMangaRef ref,
.toList();
}
}
/***********/
/*mangakawaii*/
/***********/
else if (source == "mangakawaii") {
final dom = await httpResToDom(
url:
'https://www.mangakawaii.io/search?query=${query.trim()}&search_type=manga',
headers: {'Accept-Language': 'fr'});
if (dom
.querySelectorAll(
'#page-content > div > div > ul > li > div.section__list-group-right > div.section__list-group-header > div > h4 > a')
.isNotEmpty) {
final ur = dom
.querySelectorAll(
'#page-content > div > div > ul > li > div.section__list-group-right > div.section__list-group-header > div > h4 > a')
.where((e) => e.attributes.containsKey('href'))
.map((e) => e.attributes['href'])
.toList();
for (var a in ur) {
url.add(a!);
}
final nam = dom
.querySelectorAll(
'#page-content > div > div > ul > li > div.section__list-group-right > div.section__list-group-header > div > h4 > a')
.map((e) => e.text)
.toList();
for (var a in nam) {
name.add(a);
image.add('');
}
}
}
/***********/
/*mmrcms*/
/***********/
else if (getWpMangTypeSource(source) == TypeSource.mmrcms) {
final response = await http.get(
Uri.parse('${getWpMangaUrl(source)}/search?query=${query.trim()}'));
final rep = jsonDecode(response.body);
for (var ok in rep['suggestions']) {
if (source == 'Read Comics Online') {
url.add('${getWpMangaUrl(source)}/comic/${ok['data']}');
} else if (source == 'Scan VF') {
url.add('${getWpMangaUrl(source)}/${ok['data']}');
} else {
url.add('${getWpMangaUrl(source)}/manga/${ok['data']}');
}
name.add(ok["value"]);
image.add('');
}
}
/***********/
/*mangahere*/
/***********/

View file

@ -59,6 +59,9 @@ class _BrowseScreenState extends State<BrowseScreen>
.toList();
});
},
onSuffixPressed: () {
_textEditingController.clear();
},
onPressed: () {
setState(() {
_isSearch = false;
@ -76,6 +79,10 @@ class _BrowseScreenState extends State<BrowseScreen>
setState(() {
_isSearch = true;
});
} else if (_tabBarController.index == 0) {
context.push(
'/globalSearch',
);
}
},
icon: Icon(

View file

@ -73,6 +73,5 @@ class ExtensionsLang extends ConsumerWidget {
);
}),
);
;
}
}

View file

@ -1,5 +1,3 @@
import 'dart:developer';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:mangayomi/utils/lang.dart';
@ -20,7 +18,6 @@ class ExtensionListTileWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
// log(logoUrl);
return ListTile(
onTap: () {
onChanged(!value);
@ -59,6 +56,5 @@ class ExtensionListTileWidget extends StatelessWidget {
onChanged: (value) {
onChanged(value);
}));
;
}
}

View file

@ -0,0 +1,247 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:mangayomi/models/model_manga.dart';
import 'package:mangayomi/providers/hive_provider.dart';
import 'package:mangayomi/services/get_manga_detail.dart';
import 'package:mangayomi/services/search_manga.dart';
import 'package:mangayomi/source/source_model.dart';
import 'package:mangayomi/utils/cached_network.dart';
import 'package:mangayomi/utils/headers.dart';
import 'package:mangayomi/utils/lang.dart';
import 'package:mangayomi/views/library/search_text_form_field.dart';
import 'package:mangayomi/views/manga/home/manga_home_screen.dart';
import 'package:mangayomi/views/widgets/bottom_text_widget.dart';
import 'package:mangayomi/views/widgets/manga_image_card_widget.dart';
class GlobalSearchScreen extends ConsumerStatefulWidget {
const GlobalSearchScreen({
super.key,
});
@override
ConsumerState<GlobalSearchScreen> createState() => _GlobalSearchScreenState();
}
class _GlobalSearchScreenState extends ConsumerState<GlobalSearchScreen> {
String query = "";
final _textEditingController = TextEditingController();
@override
Widget build(BuildContext context) {
final sourceList = ref
.watch(hiveBoxMangaSourceProvider)
.values
.where((element) => element.isAdded == true)
.toList();
return Scaffold(
appBar: AppBar(
leading: Container(),
actions: [
SeachFormTextField(
onChanged: (value) {},
onPressed: () {
Navigator.pop(context);
},
onFieldSubmitted: (value) {
setState(() {
query = value;
});
},
onSuffixPressed: () {
_textEditingController.clear();
setState(() {
query = "";
});
},
controller: _textEditingController,
)
],
),
body: query.isNotEmpty
? ListView(
children: [
for (var i = 0; i < sourceList.length; i++)
SizedBox(
height: 230,
child: SourceSearchScreen(
query: query,
source: sourceList[i],
),
),
],
)
: Container(),
);
}
}
class SourceSearchScreen extends ConsumerWidget {
final String query;
final SourceModel source;
const SourceSearchScreen({
super.key,
required this.query,
required this.source,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final search =
ref.watch(searchMangaProvider(source: source.sourceName, query: query));
return Scaffold(
body: SizedBox(
height: 240,
child: Column(
children: [
ListTile(
dense: true,
onTap: () {
Map<String, dynamic> data = {
'query': query,
'source': source.sourceName,
'lang': source.lang,
'viewOnly': true,
};
context.push('/searchResult', extra: data);
},
title: Text(source.sourceName),
subtitle: Text(
completeLang(source.lang),
style: const TextStyle(fontSize: 10),
),
trailing: const Icon(Icons.arrow_forward_sharp),
),
Flexible(
child: search.when(
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) =>
Center(child: Text(error.toString())),
data: (data) {
if (data.name.isNotEmpty) {
return ListView.builder(
scrollDirection: Axis.horizontal,
itemCount: data.name.length,
itemBuilder: (context, index) {
return MangaGlobalImageCard(
url: data.url[index]!,
name: data.name[index]!,
image: data.image[index]!,
source: source.sourceName,
lang: source.lang,
);
},
);
}
return const Center(
child: Text("No result"),
);
}),
),
],
),
));
}
}
class MangaGlobalImageCard extends ConsumerStatefulWidget {
final String image;
final String url;
final String name;
final String source;
final String lang;
const MangaGlobalImageCard({
super.key,
required this.url,
required this.name,
required this.image,
required this.source,
required this.lang,
});
@override
ConsumerState<MangaGlobalImageCard> createState() =>
_MangaGlobalImageCardState();
}
class _MangaGlobalImageCardState extends ConsumerState<MangaGlobalImageCard>
with AutomaticKeepAliveClientMixin<MangaGlobalImageCard> {
@override
Widget build(BuildContext context) {
super.build(context);
final getMangaDetail = ref.watch(getMangaDetailProvider(
source: widget.source,
imageUrl: widget.image,
title: widget.name,
url: widget.url,
lang: widget.lang));
return getMangaDetail.when(
data: (data) {
return GestureDetector(
onTap: () async {
final modelManga = ModelManga(
imageUrl: data.imageUrl,
name: data.name,
genre: data.genre,
author: data.author,
chapterDate: data.chapterDate,
chapterTitle: data.chapterTitle,
chapterUrl: data.chapterUrl,
status: data.status,
description: data.description,
favorite: false,
link: data.url,
source: data.source,
lang: widget.lang);
if (mounted) {
context.push('/manga-reader/detail', extra: modelManga);
}
},
child: SizedBox(
width: 90,
child: Column(children: [
cachedNetworkImage(
headers: headers(data.source!),
imageUrl: data.imageUrl!,
width: 80,
height: 120,
fit: BoxFit.fill),
BottomTextWidget(
fontSize: 12.0,
text: widget.name,
isLoading: true,
isComfortableGrid: true,
)
]),
),
);
},
loading: () => SizedBox(
width: 60,
child: Column(children: [
ClipRRect(
borderRadius: BorderRadius.circular(5),
child: Container(
color: Theme.of(context).cardColor,
width: 80,
height: 120,
),
),
BottomTextWidget(
fontSize: 12.0,
text: widget.name,
isLoading: true,
isComfortableGrid: true,
)
]),
),
error: (error, stackTrace) => Center(child: Text(error.toString())),
);
}
@override
bool get wantKeepAlive => true;
}

View file

@ -10,7 +10,7 @@ class MigrateScreen extends StatefulWidget {
class _MigrateScreenState extends State<MigrateScreen> {
@override
Widget build(BuildContext context) {
return Center(
return const Center(
child: Text('Migrate'),
);
}

View file

@ -42,7 +42,6 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
_isSearch
? SeachFormTextField(
onChanged: (value) {
log(value.toString());
setState(() {
entriesFilter = entriesData
.where((element) => element.modelManga.name!
@ -51,6 +50,9 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
.toList();
});
},
onSuffixPressed: () {
_textEditingController.clear();
},
onPressed: () {
setState(() {
_isSearch = false;
@ -69,7 +71,40 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
icon: Icon(Icons.search, color: Theme.of(context).hintColor)),
IconButton(
splashRadius: 20,
onPressed: () {},
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text(
"Remove everything",
),
content: const Text(
'Are you sure? All history will be lost.'),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text("Cancel")),
const SizedBox(
width: 15,
),
TextButton(
onPressed: () {
ref.watch(hiveBoxMangaHistory).clear();
Navigator.pop(context);
},
child: const Text("Ok")),
],
)
],
);
});
},
icon: Icon(Icons.delete_sweep_outlined,
color: Theme.of(context).hintColor)),
],
@ -80,6 +115,7 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
valueListenable: ref.watch(hiveBoxMangaHistory).listenable(),
builder: (context, value, child) {
final entries = value.values.toList();
entriesData = value.values.toList();
final entriesHistory = _textEditingController.text.isNotEmpty
? entriesFilter
: entries;
@ -212,13 +248,14 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
builder: (context) {
return AlertDialog(
title: const Text(
"Delete",
"Remove",
),
content: const Text(
'This will remove the read date of this chapter. Are you sure?'),
actions: [
Row(
mainAxisAlignment:
MainAxisAlignment
.spaceAround,
MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
@ -226,7 +263,10 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
context);
},
child: const Text(
"No")),
"Cancel")),
const SizedBox(
width: 15,
),
TextButton(
onPressed: () {
ref
@ -239,7 +279,7 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
context);
},
child: const Text(
"Yes")),
"Remove")),
],
)
],
@ -266,7 +306,9 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
order: GroupedListOrder.DESC,
);
}
return const Center(child: Text(""));
return const Center(
child: Text('Nothing read recently'),
);
},
),
),

View file

@ -60,6 +60,9 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
_textEditingController.clear();
},
controller: _textEditingController,
onSuffixPressed: () {
_textEditingController.clear();
},
)
: IconButton(
splashRadius: 20,

View file

@ -24,7 +24,7 @@ final libraryReverseListStateProvider =
typedef _$LibraryReverseListState = AutoDisposeNotifier<bool>;
String _$libraryDisplayTypeStateHash() =>
r'e3c78daf9932930aa9df05a2718d087089744522';
r'2743481e54668fe95294194b62014f9713de2cb8';
/// See also [LibraryDisplayTypeState].
@ProviderFor(LibraryDisplayTypeState)

View file

@ -3,12 +3,16 @@ import 'package:flutter/material.dart';
class SeachFormTextField extends StatelessWidget {
final Function(String)? onChanged;
final VoidCallback onPressed;
final VoidCallback onSuffixPressed;
final TextEditingController controller;
final Function(String)? onFieldSubmitted;
const SeachFormTextField(
{super.key,
required this.onChanged,
required this.onPressed,
required this.controller});
required this.controller,
this.onFieldSubmitted,
required this.onSuffixPressed});
@override
Widget build(BuildContext context) {
@ -18,6 +22,7 @@ class SeachFormTextField extends StatelessWidget {
controller: controller,
keyboardType: TextInputType.text,
onChanged: onChanged,
onFieldSubmitted: onFieldSubmitted,
decoration: InputDecoration(
isDense: true,
hintText: 'Search...',
@ -28,6 +33,8 @@ class SeachFormTextField extends StatelessWidget {
icon: const Icon(
Icons.arrow_back,
)),
suffixIcon: IconButton(
onPressed: onSuffixPressed, icon: const Icon(Icons.clear)),
enabledBorder: const OutlineInputBorder(
borderSide: BorderSide.none,
),

View file

@ -85,6 +85,5 @@ class LibraryGridViewWidget extends StatelessWidget {
);
},
);
;
}
}

View file

@ -82,6 +82,5 @@ class LibraryListViewWidget extends StatelessWidget {
);
},
);
;
}
}

View file

@ -43,7 +43,7 @@ class _MangaReaderDetailState extends ConsumerState<MangaReaderDetail> {
.watch(getMangaDetailProvider(
imageUrl: '',
lang: widget.modelManga.lang!,
name: widget.modelManga.name!,
title: widget.modelManga.name!,
source: widget.modelManga.source!,
url: widget.modelManga.link!)
.future)

View file

@ -160,7 +160,7 @@ class _MangaHomeImageCardState extends ConsumerState<MangaHomeImageCard>
final getMangaDetail = ref.watch(getMangaDetailProvider(
source: widget.source,
imageUrl: widget.image,
name: widget.name,
title: widget.name,
url: widget.url,
lang: widget.lang));

View file

@ -52,7 +52,7 @@ class CustomSearchDelegate extends SearchDelegate {
return const Center(child: Text("Empty"));
}
return SearchResult(
return SearchResultScreen(
query: query,
source: source,
lang: lang,
@ -86,43 +86,51 @@ class CustomSearchDelegate extends SearchDelegate {
}
}
class SearchResult extends ConsumerWidget {
class SearchResultScreen extends ConsumerWidget {
final String query;
final String source;
final String lang;
const SearchResult({
final bool viewOnly;
const SearchResultScreen({
super.key,
required this.query,
required this.source,
required this.lang,
this.viewOnly = false,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final search = ref.watch(searchMangaProvider(source: source, query: query));
return search.when(
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => Center(child: Text(error.toString())),
data: (data) {
if (data.name.isNotEmpty) {
return GridViewWidget(
itemCount: data.name.length,
itemBuilder: (context, index) {
return MangaHomeImageCard(
url: data.url[index]!,
name: data.name[index]!,
image: data.image[index]!,
source: source,
lang: lang,
return Scaffold(
appBar: viewOnly
? AppBar(
title: Text(query),
)
: null,
body: search.when(
loading: () => const Center(
child: CircularProgressIndicator(),
),
error: (error, stackTrace) => Center(child: Text(error.toString())),
data: (data) {
if (data.name.isNotEmpty) {
return GridViewWidget(
itemCount: data.name.length,
itemBuilder: (context, index) {
return MangaHomeImageCard(
url: data.url[index]!,
name: data.name[index]!,
image: data.image[index]!,
source: source,
lang: lang,
);
},
);
},
);
}
return const Center(
child: Text("Empty"),
);
});
}
return const Center(
child: Text("Empty"),
);
}));
}
}

View file

@ -182,7 +182,7 @@ class CurrentIndexProvider
}
}
String _$readerControllerHash() => r'a2acca315e2e89858de09af86f749a3a9ee2a9b9';
String _$readerControllerHash() => r'8d9f171beadfd98a701e370a4f9a8e145d6b31de';
abstract class _$ReaderController extends BuildlessAutoDisposeNotifier<void> {
late final MangaReaderModel mangaReaderModel;

View file

@ -20,6 +20,9 @@ class UpdatesScreen extends StatelessWidget {
icon: Icon(Icons.refresh, color: Theme.of(context).hintColor)),
],
),
body: const Center(
child: Text("No recent updates"),
),
);
}
}

View file

@ -4,11 +4,13 @@ class BottomTextWidget extends StatelessWidget {
final bool isLoading;
final String text;
final bool isComfortableGrid;
final double? fontSize;
const BottomTextWidget(
{super.key,
required this.text,
this.isLoading = false,
this.isComfortableGrid = false});
this.isComfortableGrid = false,
this.fontSize = 13.0});
@override
Widget build(BuildContext context) {
@ -20,10 +22,10 @@ class BottomTextWidget extends StatelessWidget {
Expanded(
child: Text(
text,
style: const TextStyle(
fontSize: 13.0,
style: TextStyle(
fontSize: fontSize,
color: Colors.white,
shadows: <Shadow>[
shadows: const [
Shadow(offset: Offset(0.5, 0.9), blurRadius: 3.0)
],
),

View file

@ -241,14 +241,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
draggable_menu:
dependency: "direct main"
description:
name: draggable_menu
sha256: "534e9fd85c9e37849456a8359561a6814f2b8048e04be7d97d3cdc76224ff33d"
url: "https://pub.dev"
source: hosted
version: "0.2.0"
draggable_scrollbar:
dependency: "direct main"
description:

View file

@ -52,7 +52,7 @@ dependencies:
intl: ^0.18.0
# rive: ^0.10.3
google_fonts: ^4.0.3
draggable_menu: ^0.2.0
# draggable_menu: ^0.2.0
# The following adds the Cupertino Icons font to your application.