mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-04-21 11:51:57 +00:00
feat: search filter feature
This commit is contained in:
parent
8440b25935
commit
09b002be8b
12 changed files with 1536 additions and 39 deletions
1034
lib/eval/bridge/filter.dart
Normal file
1034
lib/eval/bridge/filter.dart
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -2,12 +2,14 @@ import 'dart:convert';
|
||||||
import 'package:dart_eval/dart_eval.dart';
|
import 'package:dart_eval/dart_eval.dart';
|
||||||
import 'package:dart_eval/dart_eval_bridge.dart';
|
import 'package:dart_eval/dart_eval_bridge.dart';
|
||||||
import 'package:dart_eval/stdlib/core.dart';
|
import 'package:dart_eval/stdlib/core.dart';
|
||||||
|
import 'package:mangayomi/eval/bridge/filter.dart';
|
||||||
import 'package:mangayomi/eval/bridge/m_source.dart';
|
import 'package:mangayomi/eval/bridge/m_source.dart';
|
||||||
import 'package:mangayomi/eval/bridge/m_manga.dart';
|
import 'package:mangayomi/eval/bridge/m_manga.dart';
|
||||||
import 'package:mangayomi/eval/bridge/m_pages.dart';
|
import 'package:mangayomi/eval/bridge/m_pages.dart';
|
||||||
import 'package:mangayomi/eval/bridge/m_status.dart';
|
import 'package:mangayomi/eval/bridge/m_status.dart';
|
||||||
import 'package:mangayomi/eval/bridge/m_track.dart';
|
import 'package:mangayomi/eval/bridge/m_track.dart';
|
||||||
import 'package:mangayomi/eval/bridge/m_video.dart';
|
import 'package:mangayomi/eval/bridge/m_video.dart';
|
||||||
|
import 'package:mangayomi/eval/model/filter.dart';
|
||||||
import 'package:mangayomi/eval/model/m_bridge.dart';
|
import 'package:mangayomi/eval/model/m_bridge.dart';
|
||||||
import 'package:mangayomi/eval/model/m_pages.dart';
|
import 'package:mangayomi/eval/model/m_pages.dart';
|
||||||
import 'package:mangayomi/eval/model/m_source.dart';
|
import 'package:mangayomi/eval/model/m_source.dart';
|
||||||
|
|
@ -127,6 +129,9 @@ class $MProvider extends MProvider with $Bridge<MProvider> {
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
])),
|
])),
|
||||||
|
'getFilterList': BridgeMethodDef(BridgeFunctionDef(
|
||||||
|
returns: BridgeTypeAnnotation(BridgeTypeRef(
|
||||||
|
CoreTypes.future, [BridgeTypeRef(CoreTypes.dynamic)])))),
|
||||||
'cryptoHandler': BridgeMethodDef(
|
'cryptoHandler': BridgeMethodDef(
|
||||||
BridgeFunctionDef(
|
BridgeFunctionDef(
|
||||||
returns: BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.string)),
|
returns: BridgeTypeAnnotation(BridgeTypeRef(CoreTypes.string)),
|
||||||
|
|
@ -963,9 +968,14 @@ class $MProvider extends MProvider with $Bridge<MProvider> {
|
||||||
await $_invoke('getPopular', [$MSource.wrap(source), $int(page)]);
|
await $_invoke('getPopular', [$MSource.wrap(source), $int(page)]);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<MPages> search(MSource source, String query, int page) async =>
|
Future<MPages> search(MSource source, String query, int page,
|
||||||
await $_invoke(
|
FilterList filterList) async =>
|
||||||
'search', [$MSource.wrap(source), $String(query), $int(page)]);
|
await $_invoke('search', [
|
||||||
|
$MSource.wrap(source),
|
||||||
|
$String(query),
|
||||||
|
$int(page),
|
||||||
|
$FilterList.wrap(FilterList(_toValueList(filterList.filters)))
|
||||||
|
]);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<String>> getPageList(MSource source, String url) async {
|
Future<List<String>> getPageList(MSource source, String url) async {
|
||||||
|
|
@ -977,6 +987,15 @@ class $MProvider extends MProvider with $Bridge<MProvider> {
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List getFilterList() {
|
||||||
|
final res = $_invoke('getFilterList', []);
|
||||||
|
if (res is $List) {
|
||||||
|
return res.$reified;
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Video>> getVideoList(MSource source, String url) async {
|
Future<List<Video>> getVideoList(MSource source, String url) async {
|
||||||
final res =
|
final res =
|
||||||
|
|
@ -1000,4 +1019,35 @@ class $MProvider extends MProvider with $Bridge<MProvider> {
|
||||||
: e.audios!
|
: e.audios!
|
||||||
.map((t) => $MTrack.wrap(Track(file: t.file, label: t.label)))
|
.map((t) => $MTrack.wrap(Track(file: t.file, label: t.label)))
|
||||||
.toList()));
|
.toList()));
|
||||||
|
_toValueList(List filters) {
|
||||||
|
return (filters).map((e) {
|
||||||
|
if (e is SelectFilter) {
|
||||||
|
return $SelectFilter
|
||||||
|
.wrap(SelectFilter(e.type, e.name, e.state, _toValueList(e.values)));
|
||||||
|
} else if (e is TextFilter) {
|
||||||
|
return $TextFilter.wrap(e);
|
||||||
|
} else if (e is HeaderFilter) {
|
||||||
|
return $HeaderFilter.wrap(e);
|
||||||
|
} else if (e is CheckBoxFilter) {
|
||||||
|
return $CheckBoxFilter.wrap(e);
|
||||||
|
} else if (e is TriStateFilter) {
|
||||||
|
return $TriStateFilter.wrap(e);
|
||||||
|
} else if (e is SeparatorFilter) {
|
||||||
|
return $SeparatorFilter.wrap(e);
|
||||||
|
} else if (e is SortFilter) {
|
||||||
|
return $SortFilter
|
||||||
|
.wrap(SortFilter(e.type, e.name, e.state, _toValueList(e.values)));
|
||||||
|
} else if (e is SortState) {
|
||||||
|
return $SortState.wrap(e);
|
||||||
|
} else if (e is CheckBoxFilter) {
|
||||||
|
return $CheckBoxFilter.wrap(e);
|
||||||
|
} else if (e is SelectFilterOption) {
|
||||||
|
return $SelectFilterOption.wrap(e);
|
||||||
|
} else if (e is GroupFilter) {
|
||||||
|
return $GroupFilter
|
||||||
|
.wrap(GroupFilter(e.type, e.name, _toValueList(e.state)));
|
||||||
|
}
|
||||||
|
return e;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
92
lib/eval/model/filter.dart
Normal file
92
lib/eval/model/filter.dart
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
class FilterList {
|
||||||
|
List<dynamic> filters;
|
||||||
|
FilterList(this.filters);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectFilter {
|
||||||
|
String? type;
|
||||||
|
String name;
|
||||||
|
int state;
|
||||||
|
List<dynamic> values;
|
||||||
|
|
||||||
|
SelectFilter(this.type, this.name, this.state, this.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SelectFilterOption {
|
||||||
|
String name;
|
||||||
|
String value;
|
||||||
|
|
||||||
|
SelectFilterOption(this.name, this.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SeparatorFilter {
|
||||||
|
String? type;
|
||||||
|
SeparatorFilter({this.type = ''});
|
||||||
|
}
|
||||||
|
|
||||||
|
class HeaderFilter {
|
||||||
|
String? type;
|
||||||
|
String name;
|
||||||
|
HeaderFilter(this.name, {this.type = ''});
|
||||||
|
}
|
||||||
|
|
||||||
|
class TextFilter {
|
||||||
|
String? type;
|
||||||
|
String name;
|
||||||
|
String state;
|
||||||
|
|
||||||
|
TextFilter(this.type, this.name, {this.state = ""});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SortFilter {
|
||||||
|
String? type;
|
||||||
|
String name;
|
||||||
|
SortState state;
|
||||||
|
List<dynamic> values;
|
||||||
|
|
||||||
|
SortFilter(this.type, this.name, this.state, this.values);
|
||||||
|
}
|
||||||
|
|
||||||
|
class SortState {
|
||||||
|
int index;
|
||||||
|
bool ascending;
|
||||||
|
|
||||||
|
SortState(this.index, this.ascending);
|
||||||
|
}
|
||||||
|
|
||||||
|
class TriStateFilter {
|
||||||
|
String? type;
|
||||||
|
String name;
|
||||||
|
String value;
|
||||||
|
int state;
|
||||||
|
|
||||||
|
factory TriStateFilter.fromJson(Map<String, dynamic> json) {
|
||||||
|
return TriStateFilter(json['type'], json['name'], json['value']);
|
||||||
|
}
|
||||||
|
TriStateFilter(this.type, this.name, this.value, {this.state = 0});
|
||||||
|
}
|
||||||
|
|
||||||
|
extension TriStateFilterExtension on TriStateFilter {
|
||||||
|
bool get ignored => state == 0;
|
||||||
|
bool get included => state == 1;
|
||||||
|
bool get excluded => state == 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
class GroupFilter {
|
||||||
|
String? type;
|
||||||
|
String name;
|
||||||
|
List<dynamic> state;
|
||||||
|
|
||||||
|
GroupFilter(this.type, this.name, this.state);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CheckBoxFilter {
|
||||||
|
String? type;
|
||||||
|
String name;
|
||||||
|
String value;
|
||||||
|
bool state;
|
||||||
|
factory CheckBoxFilter.fromJson(Map<String, dynamic> json) {
|
||||||
|
return CheckBoxFilter(json['type'], json['name'], json['value']);
|
||||||
|
}
|
||||||
|
CheckBoxFilter(this.type, this.name, this.value, {this.state = false});
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:mangayomi/eval/model/filter.dart';
|
||||||
import 'package:mangayomi/eval/model/m_pages.dart';
|
import 'package:mangayomi/eval/model/m_pages.dart';
|
||||||
import 'package:mangayomi/eval/model/m_source.dart';
|
import 'package:mangayomi/eval/model/m_source.dart';
|
||||||
import 'package:mangayomi/eval/model/m_manga.dart';
|
import 'package:mangayomi/eval/model/m_manga.dart';
|
||||||
|
|
@ -10,7 +11,8 @@ abstract class MProvider {
|
||||||
|
|
||||||
Future<MPages> getPopular(MSource source, int page);
|
Future<MPages> getPopular(MSource source, int page);
|
||||||
|
|
||||||
Future<MPages> search(MSource source, String query, int page);
|
Future<MPages> search(
|
||||||
|
MSource source, String query, int page, FilterList filterList);
|
||||||
|
|
||||||
Future<MManga> getDetail(MSource source, String url);
|
Future<MManga> getDetail(MSource source, String url);
|
||||||
|
|
||||||
|
|
@ -18,5 +20,5 @@ abstract class MProvider {
|
||||||
|
|
||||||
Future<List<Video>> getVideoList(MSource source, String url);
|
Future<List<Video>> getVideoList(MSource source, String url);
|
||||||
|
|
||||||
// FilterList getFilterList();
|
List<dynamic> getFilterList();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:dart_eval/dart_eval.dart';
|
import 'package:dart_eval/dart_eval.dart';
|
||||||
import 'package:dart_eval/dart_eval_bridge.dart';
|
import 'package:dart_eval/dart_eval_bridge.dart';
|
||||||
import 'package:mangayomi/eval/bridge/m_chapter.dart';
|
import 'package:mangayomi/eval/bridge/m_chapter.dart';
|
||||||
|
import 'package:mangayomi/eval/bridge/filter.dart';
|
||||||
import 'package:mangayomi/eval/bridge/m_pages.dart';
|
import 'package:mangayomi/eval/bridge/m_pages.dart';
|
||||||
import 'package:mangayomi/eval/bridge/m_status.dart';
|
import 'package:mangayomi/eval/bridge/m_status.dart';
|
||||||
import 'package:mangayomi/eval/bridge/m_provider.dart';
|
import 'package:mangayomi/eval/bridge/m_provider.dart';
|
||||||
|
|
@ -23,6 +24,18 @@ class MEvalPlugin extends EvalPlugin {
|
||||||
registry.defineBridgeClass($MChapter.$declaration);
|
registry.defineBridgeClass($MChapter.$declaration);
|
||||||
registry.defineBridgeClass($MManga.$declaration);
|
registry.defineBridgeClass($MManga.$declaration);
|
||||||
registry.defineBridgeEnum($MStatus.$declaration);
|
registry.defineBridgeEnum($MStatus.$declaration);
|
||||||
|
//Filter
|
||||||
|
registry.defineBridgeClass($FilterList.$declaration);
|
||||||
|
registry.defineBridgeClass($SelectFilter.$declaration);
|
||||||
|
registry.defineBridgeClass($SeparatorFilter.$declaration);
|
||||||
|
registry.defineBridgeClass($HeaderFilter.$declaration);
|
||||||
|
registry.defineBridgeClass($TextFilter.$declaration);
|
||||||
|
registry.defineBridgeClass($SortFilter.$declaration);
|
||||||
|
registry.defineBridgeClass($TriStateFilter.$declaration);
|
||||||
|
registry.defineBridgeClass($GroupFilter.$declaration);
|
||||||
|
registry.defineBridgeClass($CheckBoxFilter.$declaration);
|
||||||
|
registry.defineBridgeClass($SortState.$declaration);
|
||||||
|
registry.defineBridgeClass($SelectFilterOption.$declaration);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -44,5 +57,28 @@ class MEvalPlugin extends EvalPlugin {
|
||||||
'package:mangayomi/bridge_lib.dart', 'MTrack.', $MTrack.$new);
|
'package:mangayomi/bridge_lib.dart', 'MTrack.', $MTrack.$new);
|
||||||
runtime.registerBridgeEnumValues(
|
runtime.registerBridgeEnumValues(
|
||||||
'package:mangayomi/bridge_lib.dart', 'MStatus', $MStatus.$values);
|
'package:mangayomi/bridge_lib.dart', 'MStatus', $MStatus.$values);
|
||||||
|
//Filter
|
||||||
|
runtime.registerBridgeFunc(
|
||||||
|
'package:mangayomi/bridge_lib.dart', 'FilterList.', $FilterList.$new);
|
||||||
|
runtime.registerBridgeFunc('package:mangayomi/bridge_lib.dart',
|
||||||
|
'SelectFilter.', $SelectFilter.$new);
|
||||||
|
runtime.registerBridgeFunc('package:mangayomi/bridge_lib.dart',
|
||||||
|
'SeparatorFilter.', $SeparatorFilter.$new);
|
||||||
|
runtime.registerBridgeFunc('package:mangayomi/bridge_lib.dart',
|
||||||
|
'HeaderFilter.', $HeaderFilter.$new);
|
||||||
|
runtime.registerBridgeFunc(
|
||||||
|
'package:mangayomi/bridge_lib.dart', 'TextFilter.', $TextFilter.$new);
|
||||||
|
runtime.registerBridgeFunc(
|
||||||
|
'package:mangayomi/bridge_lib.dart', 'SortFilter.', $SortFilter.$new);
|
||||||
|
runtime.registerBridgeFunc('package:mangayomi/bridge_lib.dart',
|
||||||
|
'TriStateFilter.', $TriStateFilter.$new);
|
||||||
|
runtime.registerBridgeFunc(
|
||||||
|
'package:mangayomi/bridge_lib.dart', 'GroupFilter.', $GroupFilter.$new);
|
||||||
|
runtime.registerBridgeFunc('package:mangayomi/bridge_lib.dart',
|
||||||
|
'CheckBoxFilter.', $CheckBoxFilter.$new);
|
||||||
|
runtime.registerBridgeFunc(
|
||||||
|
'package:mangayomi/bridge_lib.dart', 'SortState.', $SortState.$new);
|
||||||
|
runtime.registerBridgeFunc('package:mangayomi/bridge_lib.dart',
|
||||||
|
'SelectFilterOption.', $SelectFilterOption.$new);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -112,11 +112,8 @@ class SourceSearchScreen extends ConsumerWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final l10n = l10nLocalizations(context)!;
|
final l10n = l10nLocalizations(context)!;
|
||||||
final search = ref.watch(searchProvider(
|
final search = ref.watch(
|
||||||
source: source,
|
searchProvider(source: source, page: 1, query: query, filterList: []));
|
||||||
page: 1,
|
|
||||||
query: query,
|
|
||||||
));
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: SizedBox(
|
body: SizedBox(
|
||||||
height: 260,
|
height: 260,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,10 @@ import 'package:go_router/go_router.dart';
|
||||||
import 'package:mangayomi/eval/model/m_manga.dart';
|
import 'package:mangayomi/eval/model/m_manga.dart';
|
||||||
import 'package:mangayomi/eval/model/m_pages.dart';
|
import 'package:mangayomi/eval/model/m_pages.dart';
|
||||||
import 'package:mangayomi/models/source.dart';
|
import 'package:mangayomi/models/source.dart';
|
||||||
|
import 'package:mangayomi/modules/manga/home/widget/filter_widget.dart';
|
||||||
|
import 'package:mangayomi/modules/widgets/progress_center.dart';
|
||||||
import 'package:mangayomi/providers/l10n_providers.dart';
|
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||||
|
import 'package:mangayomi/services/get_filter_list.dart';
|
||||||
import 'package:mangayomi/services/get_latest_updates.dart';
|
import 'package:mangayomi/services/get_latest_updates.dart';
|
||||||
import 'package:mangayomi/services/get_popular.dart';
|
import 'package:mangayomi/services/get_popular.dart';
|
||||||
import 'package:mangayomi/services/search.dart';
|
import 'package:mangayomi/services/search.dart';
|
||||||
|
|
@ -45,6 +48,7 @@ class _MangaHomeScreenState extends ConsumerState<MangaHomeScreen> {
|
||||||
int _fullDataLength = 50;
|
int _fullDataLength = 50;
|
||||||
int _page = 1;
|
int _page = 1;
|
||||||
late int _selectedIndex = widget.isSearch ? 2 : 0;
|
late int _selectedIndex = widget.isSearch ? 2 : 0;
|
||||||
|
List<dynamic> filters = [];
|
||||||
List<TypeMangaSelector> _types(BuildContext context) {
|
List<TypeMangaSelector> _types(BuildContext context) {
|
||||||
final l10n = l10nLocalizations(context)!;
|
final l10n = l10nLocalizations(context)!;
|
||||||
return [
|
return [
|
||||||
|
|
@ -72,12 +76,15 @@ class _MangaHomeScreenState extends ConsumerState<MangaHomeScreen> {
|
||||||
source: widget.source,
|
source: widget.source,
|
||||||
page: _page + 1,
|
page: _page + 1,
|
||||||
).future);
|
).future);
|
||||||
} else if (_selectedIndex == 2 && _isSearch && _query.isNotEmpty) {
|
} else if (_selectedIndex == 2 &&
|
||||||
|
_isSearch &&
|
||||||
|
(_query.isNotEmpty || _isFiltering)) {
|
||||||
mangaResList = await ref.watch(searchProvider(
|
mangaResList = await ref.watch(searchProvider(
|
||||||
source: widget.source,
|
source: widget.source,
|
||||||
query: _query,
|
query: _query,
|
||||||
page: _page + 1,
|
page: _page + 1,
|
||||||
).future);
|
filterList: filters)
|
||||||
|
.future);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
|
|
@ -94,11 +101,15 @@ class _MangaHomeScreenState extends ConsumerState<MangaHomeScreen> {
|
||||||
late bool _isSearch = widget.isSearch;
|
late bool _isSearch = widget.isSearch;
|
||||||
AsyncValue<MPages?>? _getManga;
|
AsyncValue<MPages?>? _getManga;
|
||||||
int _length = 0;
|
int _length = 0;
|
||||||
|
bool _isFiltering = false;
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_selectedIndex == 2 && _isSearch && _query.isNotEmpty) {
|
final filterList = getFilterList(source: widget.source);
|
||||||
_getManga = ref
|
if (_selectedIndex == 2 &&
|
||||||
.watch(searchProvider(source: widget.source, query: _query, page: 1));
|
_isSearch &&
|
||||||
|
(_query.isNotEmpty || _isFiltering)) {
|
||||||
|
_getManga = ref.watch(searchProvider(
|
||||||
|
source: widget.source, query: _query, page: 1, filterList: filters));
|
||||||
} else if (_selectedIndex == 1 && !_isSearch && _query.isEmpty) {
|
} else if (_selectedIndex == 1 && !_isSearch && _query.isEmpty) {
|
||||||
_getManga =
|
_getManga =
|
||||||
ref.watch(getLatestUpdatesProvider(source: widget.source, page: 1));
|
ref.watch(getLatestUpdatesProvider(source: widget.source, page: 1));
|
||||||
|
|
@ -138,6 +149,7 @@ class _MangaHomeScreenState extends ConsumerState<MangaHomeScreen> {
|
||||||
if (_textEditingController.text.isEmpty) {
|
if (_textEditingController.text.isEmpty) {
|
||||||
_isSearch = false;
|
_isSearch = false;
|
||||||
_query = "";
|
_query = "";
|
||||||
|
_isFiltering = false;
|
||||||
_selectedIndex = 0;
|
_selectedIndex = 0;
|
||||||
_page = 1;
|
_page = 1;
|
||||||
_textEditingController.clear();
|
_textEditingController.clear();
|
||||||
|
|
@ -189,11 +201,75 @@ class _MangaHomeScreenState extends ConsumerState<MangaHomeScreen> {
|
||||||
icon: _types(context)[index].icon,
|
icon: _types(context)[index].icon,
|
||||||
selected: _selectedIndex == index,
|
selected: _selectedIndex == index,
|
||||||
text: _types(context)[index].title,
|
text: _types(context)[index].title,
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
setState(() {
|
if (filters.isEmpty) {
|
||||||
_selectedIndex = index;
|
filters = filterList;
|
||||||
_page = 1;
|
}
|
||||||
});
|
if (filters.isNotEmpty && index == 2) {
|
||||||
|
final result = await showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
builder: (context) =>
|
||||||
|
StatefulBuilder(builder: (context, setState) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
filters = getFilterList(
|
||||||
|
source: widget.source);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
child: Text(l10n.reset),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context, 'filter');
|
||||||
|
},
|
||||||
|
child: Text(l10n.filter),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(),
|
||||||
|
Expanded(
|
||||||
|
child: FilterWidget(
|
||||||
|
filterList: filters,
|
||||||
|
onChanged: (values) {
|
||||||
|
setState(() {
|
||||||
|
filters = values;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
if (result == 'filter') {
|
||||||
|
setState(() {
|
||||||
|
_selectedIndex = 2;
|
||||||
|
_isFiltering = true;
|
||||||
|
_isSearch = true;
|
||||||
|
_page = 1;
|
||||||
|
});
|
||||||
|
_getManga = ref.refresh(searchProvider(
|
||||||
|
source: widget.source,
|
||||||
|
query: _query,
|
||||||
|
page: 1,
|
||||||
|
filterList: filters));
|
||||||
|
}
|
||||||
|
} else if (index != 2) {
|
||||||
|
setState(() {
|
||||||
|
_selectedIndex = index;
|
||||||
|
_isFiltering = false;
|
||||||
|
_page = 1;
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
@ -215,6 +291,9 @@ class _MangaHomeScreenState extends ConsumerState<MangaHomeScreen> {
|
||||||
},
|
},
|
||||||
child: _getManga!.when(
|
child: _getManga!.when(
|
||||||
data: (data) {
|
data: (data) {
|
||||||
|
if (_getManga!.isLoading) {
|
||||||
|
return const ProgressCenter();
|
||||||
|
}
|
||||||
Widget buildProgressIndicator() {
|
Widget buildProgressIndicator() {
|
||||||
return !(data!.list.isNotEmpty && (data.hasNextPage))
|
return !(data!.list.isNotEmpty && (data.hasNextPage))
|
||||||
? Container()
|
? Container()
|
||||||
|
|
@ -334,11 +413,12 @@ class _MangaHomeScreenState extends ConsumerState<MangaHomeScreen> {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
if (_selectedIndex == 2 &&
|
if (_selectedIndex == 2 &&
|
||||||
_isSearch &&
|
_isSearch &&
|
||||||
_query.isNotEmpty) {
|
(_query.isNotEmpty || _isFiltering)) {
|
||||||
ref.invalidate(searchProvider(
|
ref.invalidate(searchProvider(
|
||||||
source: widget.source,
|
source: widget.source,
|
||||||
query: _query,
|
query: _query,
|
||||||
page: 1));
|
page: 1,
|
||||||
|
filterList: filters));
|
||||||
} else if (_selectedIndex == 1 &&
|
} else if (_selectedIndex == 1 &&
|
||||||
!_isSearch &&
|
!_isSearch &&
|
||||||
_query.isEmpty) {
|
_query.isEmpty) {
|
||||||
|
|
|
||||||
163
lib/modules/manga/home/widget/filter_widget.dart
Normal file
163
lib/modules/manga/home/widget/filter_widget.dart
Normal file
|
|
@ -0,0 +1,163 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:mangayomi/eval/model/filter.dart';
|
||||||
|
import 'package:mangayomi/utils/colors.dart';
|
||||||
|
|
||||||
|
class FilterWidget extends StatelessWidget {
|
||||||
|
final List<dynamic> filterList;
|
||||||
|
final Function(List<dynamic>) onChanged;
|
||||||
|
const FilterWidget(
|
||||||
|
{super.key, required this.onChanged, required this.filterList});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
itemCount: filterList.length,
|
||||||
|
primary: false,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemBuilder: (context, idx) {
|
||||||
|
final filterState = filterList[idx];
|
||||||
|
Widget? widget;
|
||||||
|
if (filterState is TextFilter) {
|
||||||
|
widget = Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: TextField(
|
||||||
|
onChanged: (val) {
|
||||||
|
filterList[idx] = filterState..state = val;
|
||||||
|
onChanged(filterList);
|
||||||
|
},
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: secondaryColor(context)),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderSide: BorderSide(color: primaryColor(context)),
|
||||||
|
),
|
||||||
|
border: const OutlineInputBorder(borderSide: BorderSide()),
|
||||||
|
labelText: filterState.name,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else if (filterState is HeaderFilter) {
|
||||||
|
widget = ListTile(dense: true, title: Text(filterState.name));
|
||||||
|
} else if (filterState is SeparatorFilter) {
|
||||||
|
widget = const Divider();
|
||||||
|
} else if (filterState is TriStateFilter) {
|
||||||
|
final state = filterState.state;
|
||||||
|
widget = CheckboxListTile(
|
||||||
|
dense: true,
|
||||||
|
value: state == 0
|
||||||
|
? false
|
||||||
|
: state == 1
|
||||||
|
? true
|
||||||
|
: null,
|
||||||
|
onChanged: (value) {
|
||||||
|
filterList[idx] = filterState
|
||||||
|
..state = value == null
|
||||||
|
? 2
|
||||||
|
: value == true
|
||||||
|
? 1
|
||||||
|
: 0;
|
||||||
|
onChanged(filterList);
|
||||||
|
},
|
||||||
|
title: Text(filterState.name),
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
tristate: true,
|
||||||
|
);
|
||||||
|
} else if (filterState is CheckBoxFilter) {
|
||||||
|
widget = CheckboxListTile(
|
||||||
|
dense: true,
|
||||||
|
value: filterState.state,
|
||||||
|
onChanged: (value) {
|
||||||
|
filterList[idx] = filterState..state = value!;
|
||||||
|
onChanged(filterList);
|
||||||
|
},
|
||||||
|
title: Text(filterState.name),
|
||||||
|
controlAffinity: ListTileControlAffinity.leading,
|
||||||
|
);
|
||||||
|
} else if (filterState is GroupFilter) {
|
||||||
|
widget = ExpansionTile(
|
||||||
|
title: Text(filterState.name, style: const TextStyle(fontSize: 13)),
|
||||||
|
children: [
|
||||||
|
FilterWidget(
|
||||||
|
filterList: filterState.state,
|
||||||
|
onChanged: (values) {
|
||||||
|
filterState.state = values;
|
||||||
|
onChanged(filterList);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
} else if (filterState is SortFilter) {
|
||||||
|
final ascending = filterState.state.ascending;
|
||||||
|
widget = ExpansionTile(
|
||||||
|
title: Text(filterState.name, style: const TextStyle(fontSize: 13)),
|
||||||
|
children: filterState.values.map((e) {
|
||||||
|
final selected = filterState.values[filterState.state.index] == e;
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
leading: Icon(
|
||||||
|
ascending
|
||||||
|
? Icons.arrow_upward_rounded
|
||||||
|
: Icons.arrow_downward_rounded,
|
||||||
|
color: selected ? null : Colors.transparent),
|
||||||
|
title: Text(e.name),
|
||||||
|
onTap: () {
|
||||||
|
if (selected) {
|
||||||
|
filterState.state.ascending = !ascending;
|
||||||
|
} else {
|
||||||
|
filterState.state.index = filterState.values
|
||||||
|
.indexWhere((element) => element == e);
|
||||||
|
}
|
||||||
|
filterList[idx] = filterState;
|
||||||
|
onChanged(filterList);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
} else if (filterState is SelectFilter) {
|
||||||
|
widget = Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: ListTile(
|
||||||
|
dense: true,
|
||||||
|
title: Text(filterState.name),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: Padding(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 8, horizontal: 25),
|
||||||
|
child: DropdownButton(
|
||||||
|
icon: const Icon(Icons.keyboard_arrow_down),
|
||||||
|
isExpanded: true,
|
||||||
|
value: filterState.values[filterState.state],
|
||||||
|
hint: Text(filterState.name,
|
||||||
|
style: const TextStyle(fontSize: 13)),
|
||||||
|
items: filterState.values
|
||||||
|
.map((e) => DropdownMenuItem(
|
||||||
|
value: e,
|
||||||
|
child: Text(e.name,
|
||||||
|
style: const TextStyle(fontSize: 13)),
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
filterState.state = filterState.values
|
||||||
|
.indexWhere((element) => element == value);
|
||||||
|
onChanged(filterList);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return widget ?? const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,11 +6,11 @@ part of 'auto_backup.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$checkAndBackupHash() => r'039aea77df04d1925a25ad8839177cf9ee96aa65';
|
String _$checkAndBackupHash() => r'fad714bbc0d37442d4f72fd98d91fcaf72166284';
|
||||||
|
|
||||||
/// See also [checkAndBackup].
|
/// See also [checkAndBackup].
|
||||||
@ProviderFor(checkAndBackup)
|
@ProviderFor(checkAndBackup)
|
||||||
final checkAndBackupProvider = AutoDisposeProvider<void>.internal(
|
final checkAndBackupProvider = AutoDisposeFutureProvider<void>.internal(
|
||||||
checkAndBackup,
|
checkAndBackup,
|
||||||
name: r'checkAndBackupProvider',
|
name: r'checkAndBackupProvider',
|
||||||
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
|
||||||
|
|
@ -20,7 +20,7 @@ final checkAndBackupProvider = AutoDisposeProvider<void>.internal(
|
||||||
allTransitiveDependencies: null,
|
allTransitiveDependencies: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
typedef CheckAndBackupRef = AutoDisposeProviderRef<void>;
|
typedef CheckAndBackupRef = AutoDisposeFutureProviderRef<void>;
|
||||||
String _$backupFrequencyStateHash() =>
|
String _$backupFrequencyStateHash() =>
|
||||||
r'2e73e3fe54456978ff92f49cdc67e84f2af6de7c';
|
r'2e73e3fe54456978ff92f49cdc67e84f2af6de7c';
|
||||||
|
|
||||||
|
|
@ -39,7 +39,7 @@ final backupFrequencyStateProvider =
|
||||||
|
|
||||||
typedef _$BackupFrequencyState = AutoDisposeNotifier<int>;
|
typedef _$BackupFrequencyState = AutoDisposeNotifier<int>;
|
||||||
String _$backupFrequencyOptionsStateHash() =>
|
String _$backupFrequencyOptionsStateHash() =>
|
||||||
r'98c9a7014b56b3a2ec243f82efa3c2b5aade03fc';
|
r'79d93411a02867c8882d2d0f2143f5da6c107075';
|
||||||
|
|
||||||
/// See also [BackupFrequencyOptionsState].
|
/// See also [BackupFrequencyOptionsState].
|
||||||
@ProviderFor(BackupFrequencyOptionsState)
|
@ProviderFor(BackupFrequencyOptionsState)
|
||||||
|
|
@ -56,7 +56,7 @@ final backupFrequencyOptionsStateProvider = AutoDisposeNotifierProvider<
|
||||||
|
|
||||||
typedef _$BackupFrequencyOptionsState = AutoDisposeNotifier<List<int>>;
|
typedef _$BackupFrequencyOptionsState = AutoDisposeNotifier<List<int>>;
|
||||||
String _$autoBackupLocationStateHash() =>
|
String _$autoBackupLocationStateHash() =>
|
||||||
r'0ec954361736e570e3bc6fa4e7895fc79070cb33';
|
r'87feb5475d47a77da4e87fa62e551b785fa1e7ba';
|
||||||
|
|
||||||
/// See also [AutoBackupLocationState].
|
/// See also [AutoBackupLocationState].
|
||||||
@ProviderFor(AutoBackupLocationState)
|
@ProviderFor(AutoBackupLocationState)
|
||||||
|
|
|
||||||
27
lib/services/get_filter_list.dart
Normal file
27
lib/services/get_filter_list.dart
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import 'package:dart_eval/dart_eval_bridge.dart';
|
||||||
|
import 'package:mangayomi/eval/compiler/compiler.dart';
|
||||||
|
import 'package:mangayomi/eval/model/m_provider.dart';
|
||||||
|
import 'package:mangayomi/models/source.dart';
|
||||||
|
import 'package:mangayomi/eval/runtime/runtime.dart';
|
||||||
|
import 'package:mangayomi/sources/source_test.dart';
|
||||||
|
|
||||||
|
List<dynamic> getFilterList({required Source source}) {
|
||||||
|
List<dynamic> filterList = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
final bytecode =
|
||||||
|
compilerEval(useTestSourceCode ? testSourceCode : source.sourceCode!);
|
||||||
|
|
||||||
|
final runtime = runtimeEval(bytecode);
|
||||||
|
|
||||||
|
var res = runtime.executeLib('package:mangayomi/main.dart', 'main');
|
||||||
|
filterList = (res as MProvider)
|
||||||
|
.getFilterList()
|
||||||
|
.map((e) => e is $Value ? e.$reified : e)
|
||||||
|
.toList();
|
||||||
|
} catch (_) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return filterList;
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import 'package:mangayomi/eval/model/filter.dart';
|
||||||
import 'package:mangayomi/eval/model/m_pages.dart';
|
import 'package:mangayomi/eval/model/m_pages.dart';
|
||||||
import 'package:mangayomi/eval/compiler/compiler.dart';
|
import 'package:mangayomi/eval/compiler/compiler.dart';
|
||||||
import 'package:mangayomi/eval/model/m_provider.dart';
|
import 'package:mangayomi/eval/model/m_provider.dart';
|
||||||
|
|
@ -8,12 +9,11 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
part 'search.g.dart';
|
part 'search.g.dart';
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<MPages?> search(
|
Future<MPages?> search(SearchRef ref,
|
||||||
SearchRef ref, {
|
{required Source source,
|
||||||
required Source source,
|
required String query,
|
||||||
required String query,
|
required int page,
|
||||||
required int page,
|
required List<dynamic> filterList}) async {
|
||||||
}) async {
|
|
||||||
MPages? manga;
|
MPages? manga;
|
||||||
final bytecode =
|
final bytecode =
|
||||||
compilerEval(useTestSourceCode ? testSourceCode : source.sourceCode!);
|
compilerEval(useTestSourceCode ? testSourceCode : source.sourceCode!);
|
||||||
|
|
@ -21,7 +21,7 @@ Future<MPages?> search(
|
||||||
var res = runtime.executeLib('package:mangayomi/main.dart', 'main');
|
var res = runtime.executeLib('package:mangayomi/main.dart', 'main');
|
||||||
try {
|
try {
|
||||||
manga = await (res as MProvider)
|
manga = await (res as MProvider)
|
||||||
.search(source.toMSource(), query, page);
|
.search(source.toMSource(), query, page, FilterList(filterList));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw Exception(e);
|
throw Exception(e);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ part of 'search.dart';
|
||||||
// RiverpodGenerator
|
// RiverpodGenerator
|
||||||
// **************************************************************************
|
// **************************************************************************
|
||||||
|
|
||||||
String _$searchHash() => r'a387d6ddc86044bba9c8264c8bec6c1c29393ac5';
|
String _$searchHash() => r'b2683242cc45e83f3946339d3490cad6488dd847';
|
||||||
|
|
||||||
/// Copied from Dart SDK
|
/// Copied from Dart SDK
|
||||||
class _SystemHash {
|
class _SystemHash {
|
||||||
|
|
@ -43,11 +43,13 @@ class SearchFamily extends Family<AsyncValue<MPages?>> {
|
||||||
required Source source,
|
required Source source,
|
||||||
required String query,
|
required String query,
|
||||||
required int page,
|
required int page,
|
||||||
|
required List<dynamic> filterList,
|
||||||
}) {
|
}) {
|
||||||
return SearchProvider(
|
return SearchProvider(
|
||||||
source: source,
|
source: source,
|
||||||
query: query,
|
query: query,
|
||||||
page: page,
|
page: page,
|
||||||
|
filterList: filterList,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,6 +61,7 @@ class SearchFamily extends Family<AsyncValue<MPages?>> {
|
||||||
source: provider.source,
|
source: provider.source,
|
||||||
query: provider.query,
|
query: provider.query,
|
||||||
page: provider.page,
|
page: provider.page,
|
||||||
|
filterList: provider.filterList,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,12 +87,14 @@ class SearchProvider extends AutoDisposeFutureProvider<MPages?> {
|
||||||
required Source source,
|
required Source source,
|
||||||
required String query,
|
required String query,
|
||||||
required int page,
|
required int page,
|
||||||
|
required List<dynamic> filterList,
|
||||||
}) : this._internal(
|
}) : this._internal(
|
||||||
(ref) => search(
|
(ref) => search(
|
||||||
ref as SearchRef,
|
ref as SearchRef,
|
||||||
source: source,
|
source: source,
|
||||||
query: query,
|
query: query,
|
||||||
page: page,
|
page: page,
|
||||||
|
filterList: filterList,
|
||||||
),
|
),
|
||||||
from: searchProvider,
|
from: searchProvider,
|
||||||
name: r'searchProvider',
|
name: r'searchProvider',
|
||||||
|
|
@ -102,6 +107,7 @@ class SearchProvider extends AutoDisposeFutureProvider<MPages?> {
|
||||||
source: source,
|
source: source,
|
||||||
query: query,
|
query: query,
|
||||||
page: page,
|
page: page,
|
||||||
|
filterList: filterList,
|
||||||
);
|
);
|
||||||
|
|
||||||
SearchProvider._internal(
|
SearchProvider._internal(
|
||||||
|
|
@ -114,11 +120,13 @@ class SearchProvider extends AutoDisposeFutureProvider<MPages?> {
|
||||||
required this.source,
|
required this.source,
|
||||||
required this.query,
|
required this.query,
|
||||||
required this.page,
|
required this.page,
|
||||||
|
required this.filterList,
|
||||||
}) : super.internal();
|
}) : super.internal();
|
||||||
|
|
||||||
final Source source;
|
final Source source;
|
||||||
final String query;
|
final String query;
|
||||||
final int page;
|
final int page;
|
||||||
|
final List<dynamic> filterList;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Override overrideWith(
|
Override overrideWith(
|
||||||
|
|
@ -136,6 +144,7 @@ class SearchProvider extends AutoDisposeFutureProvider<MPages?> {
|
||||||
source: source,
|
source: source,
|
||||||
query: query,
|
query: query,
|
||||||
page: page,
|
page: page,
|
||||||
|
filterList: filterList,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -150,7 +159,8 @@ class SearchProvider extends AutoDisposeFutureProvider<MPages?> {
|
||||||
return other is SearchProvider &&
|
return other is SearchProvider &&
|
||||||
other.source == source &&
|
other.source == source &&
|
||||||
other.query == query &&
|
other.query == query &&
|
||||||
other.page == page;
|
other.page == page &&
|
||||||
|
other.filterList == filterList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -159,6 +169,7 @@ class SearchProvider extends AutoDisposeFutureProvider<MPages?> {
|
||||||
hash = _SystemHash.combine(hash, source.hashCode);
|
hash = _SystemHash.combine(hash, source.hashCode);
|
||||||
hash = _SystemHash.combine(hash, query.hashCode);
|
hash = _SystemHash.combine(hash, query.hashCode);
|
||||||
hash = _SystemHash.combine(hash, page.hashCode);
|
hash = _SystemHash.combine(hash, page.hashCode);
|
||||||
|
hash = _SystemHash.combine(hash, filterList.hashCode);
|
||||||
|
|
||||||
return _SystemHash.finish(hash);
|
return _SystemHash.finish(hash);
|
||||||
}
|
}
|
||||||
|
|
@ -173,6 +184,9 @@ mixin SearchRef on AutoDisposeFutureProviderRef<MPages?> {
|
||||||
|
|
||||||
/// The parameter `page` of this provider.
|
/// The parameter `page` of this provider.
|
||||||
int get page;
|
int get page;
|
||||||
|
|
||||||
|
/// The parameter `filterList` of this provider.
|
||||||
|
List<dynamic> get filterList;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SearchProviderElement extends AutoDisposeFutureProviderElement<MPages?>
|
class _SearchProviderElement extends AutoDisposeFutureProviderElement<MPages?>
|
||||||
|
|
@ -185,6 +199,8 @@ class _SearchProviderElement extends AutoDisposeFutureProviderElement<MPages?>
|
||||||
String get query => (origin as SearchProvider).query;
|
String get query => (origin as SearchProvider).query;
|
||||||
@override
|
@override
|
||||||
int get page => (origin as SearchProvider).page;
|
int get page => (origin as SearchProvider).page;
|
||||||
|
@override
|
||||||
|
List<dynamic> get filterList => (origin as SearchProvider).filterList;
|
||||||
}
|
}
|
||||||
// ignore_for_file: type=lint
|
// ignore_for_file: type=lint
|
||||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue