From 0ec7f8d29a23dc2d4db8ce7d8626db93d1dda269 Mon Sep 17 00:00:00 2001 From: MiaoMint <1981324730@qq.com> Date: Sat, 26 Aug 2023 21:28:23 +0800 Subject: [PATCH 1/2] Feat: Filtering system --- assets/i18n/en.json | 3 +- assets/i18n/zh.json | 3 +- lib/models/extension.dart | 22 ++ lib/models/extension.g.dart | 18 ++ lib/pages/search/pages/search_extension.dart | 207 ++++++++++++++++++- lib/utils/extension_runtime.dart | 39 +++- lib/widgets/button.dart | 43 ++++ 7 files changed, 327 insertions(+), 8 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index ed843f9b..4d83c1f6 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -46,7 +46,8 @@ "search": { "hint-text": "Please use search wisely!~", - "all": "All" + "all": "All", + "filter": "Filter" }, "extension": { diff --git a/assets/i18n/zh.json b/assets/i18n/zh.json index 823c27f1..6651bf6c 100644 --- a/assets/i18n/zh.json +++ b/assets/i18n/zh.json @@ -35,7 +35,8 @@ "search": { "hint-text": "请善用搜索哦!~", - "all": "全部" + "all": "全部", + "filter": "筛选" }, "extension": { diff --git a/lib/models/extension.dart b/lib/models/extension.dart index 19d3d093..fd7a9931 100644 --- a/lib/models/extension.dart +++ b/lib/models/extension.dart @@ -48,6 +48,28 @@ class Extension { Map toJson() => _$ExtensionToJson(this); } +@JsonSerializable() +class ExtensionFilter { + ExtensionFilter({ + required this.title, + required this.min, + required this.max, + required this.defaultOption, + required this.options, + }); + final String title; + final int min; + final int max; + @JsonKey(name: "default") + final String defaultOption; + final Map options; + + factory ExtensionFilter.fromJson(Map json) => + _$ExtensionFilterFromJson(json); + + Map toJson() => _$ExtensionFilterToJson(this); +} + @JsonSerializable() class ExtensionListItem { ExtensionListItem({ diff --git a/lib/models/extension.g.dart b/lib/models/extension.g.dart index 69c59644..7cd86304 100644 --- a/lib/models/extension.g.dart +++ b/lib/models/extension.g.dart @@ -42,6 +42,24 @@ const _$ExtensionTypeEnumMap = { ExtensionType.fikushon: 'fikushon', }; +ExtensionFilter _$ExtensionFilterFromJson(Map json) => + ExtensionFilter( + title: json['title'] as String, + min: json['min'] as int, + max: json['max'] as int, + defaultOption: json['default'] as String, + options: Map.from(json['options'] as Map), + ); + +Map _$ExtensionFilterToJson(ExtensionFilter instance) => + { + 'title': instance.title, + 'min': instance.min, + 'max': instance.max, + 'default': instance.defaultOption, + 'options': instance.options, + }; + ExtensionListItem _$ExtensionListItemFromJson(Map json) => ExtensionListItem( title: json['title'] as String, diff --git a/lib/pages/search/pages/search_extension.dart b/lib/pages/search/pages/search_extension.dart index c3e9d9e9..c2e991aa 100644 --- a/lib/pages/search/pages/search_extension.dart +++ b/lib/pages/search/pages/search_extension.dart @@ -3,11 +3,14 @@ import 'dart:io'; import 'package:easy_refresh/easy_refresh.dart'; import 'package:fluent_ui/fluent_ui.dart' as fluent; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:miru_app/models/index.dart'; +import 'package:miru_app/router/router.dart'; import 'package:miru_app/utils/extension.dart'; import 'package:miru_app/utils/extension_runtime.dart'; import 'package:miru_app/utils/i18n.dart'; +import 'package:miru_app/widgets/button.dart'; import 'package:miru_app/widgets/extension_item_card.dart'; import 'package:miru_app/widgets/infinite_scroller.dart'; import 'package:miru_app/widgets/messenger.dart'; @@ -35,6 +38,26 @@ class _SearchExtensionPageState extends fluent.State { int _page = 1; bool _isLoading = true; final EasyRefreshController _easyRefreshController = EasyRefreshController(); + Map? _filters; + // 初始化一开始选择的选项 + Map> _selectedFilters = {}; + // 缓存的选项 + + @override + void initState() { + super.initState(); + SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + _initFilters(); + }); + } + + _initFilters() async { + _filters = await _runtime.createFilter(); + _filters!.forEach((key, value) { + _selectedFilters[key] = [value.defaultOption]; + }); + setState(() {}); + } Future _onRefresh() async { setState(() { @@ -49,10 +72,10 @@ class _SearchExtensionPageState extends fluent.State { _isLoading = true; setState(() {}); late List data; - if (_keyWord.isEmpty) { + if (_keyWord.isEmpty && _filters == null) { data = await _runtime.latest(_page); } else { - data = await _runtime.search(_keyWord, _page); + data = await _runtime.search(_keyWord, _page, filter: _selectedFilters); } if (data.isEmpty && mounted) { showPlatformSnackbar( @@ -87,6 +110,76 @@ class _SearchExtensionPageState extends fluent.State { } } + _onFilter(BuildContext context) { + final fiterWidget = _ExtensionFilterWidget( + runtime: _runtime, + filters: _filters!, + selectedFilters: _selectedFilters, + onSelectFilter: (selectedFilters, filters) { + _selectedFilters = selectedFilters; + _filters = filters; + }, + ); + + if (Platform.isAndroid) { + showModalBottomSheet( + context: context, + builder: (context) => Column( + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + TextButton( + onPressed: () { + router.pop(); + }, + child: Text("common.cancel".i18n), + ), + const Spacer(), + FilledButton( + onPressed: () { + router.pop(); + _easyRefreshController.callRefresh(); + }, + child: Text("common.confirm".i18n), + ) + ], + ), + ), + Expanded(child: fiterWidget) + ], + ), + ); + return; + } + + fluent.showDialog( + context: context, + builder: (context) { + return fluent.ContentDialog( + title: Text('search.filter'.i18n), + content: fiterWidget, + actions: [ + fluent.Button( + child: Text('common.cancel'.i18n), + onPressed: () { + router.pop(); + }, + ), + fluent.FilledButton( + child: Text('common.confirm'.i18n), + onPressed: () { + router.pop(); + _onRefresh(); + }, + ), + ], + ); + }, + ); + } + Widget _buildAndroid(BuildContext context) { return Scaffold( appBar: SearchAppBar( @@ -98,6 +191,13 @@ class _SearchExtensionPageState extends fluent.State { } }, onSubmitted: _onSearch, + actions: [ + if (_filters != null) + IconButton( + icon: const Icon(Icons.filter_alt_rounded), + onPressed: () => _onFilter(context), + ), + ], ), body: InfiniteScroller( onRefresh: _onRefresh, @@ -153,6 +253,12 @@ class _SearchExtensionPageState extends fluent.State { ), ), const Spacer(), + if (_filters != null) + fluent.IconButton( + icon: const Icon(fluent.FluentIcons.filter), + onPressed: () => _onFilter(context), + ), + const SizedBox(width: 8), SizedBox( width: 300, child: fluent.TextBox( @@ -231,3 +337,100 @@ class _SearchExtensionPageState extends fluent.State { ); } } + +class _ExtensionFilterWidget extends StatefulWidget { + const _ExtensionFilterWidget({ + Key? key, + required this.runtime, + required this.selectedFilters, + required this.onSelectFilter, + required this.filters, + }) : super(key: key); + final ExtensionRuntime runtime; + final Map filters; + final Map> selectedFilters; + final Function( + Map> selectedFilters, + Map filters, + ) onSelectFilter; + + @override + State<_ExtensionFilterWidget> createState() => _ExtensionFilterWidgetState(); +} + +class _ExtensionFilterWidgetState extends State<_ExtensionFilterWidget> { + late final ExtensionRuntime _runtime = widget.runtime; + late Map _filters = widget.filters; + // 初始化一开始选择的选项 + late Map> _selectedFilters = widget.selectedFilters; + + _onSelectFilter(key, value) async { + final selectedFilters = Map>.from(_selectedFilters); + // 如果存在就删除,不存在就添加 + if (selectedFilters[key]!.contains(value)) { + if (selectedFilters[key]!.length > _filters[key]!.min) { + selectedFilters[key]!.remove(value); + } + } else { + if (selectedFilters[key]!.length >= _filters[key]!.max) { + selectedFilters[key]!.removeAt(0); + } + selectedFilters[key]!.add(value); + } + // 再请求一次 _filters + final filters = Map.from( + await _runtime.createFilter(filter: selectedFilters)); + + // 剔除 _filters 中不能存在的选项 + selectedFilters.forEach((key, value) { + if (!filters.containsKey(key)) { + selectedFilters.remove(key); + } + }); + + setState(() { + _selectedFilters = selectedFilters; + _filters = filters; + }); + widget.onSelectFilter(_selectedFilters, _filters); + } + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + for (final filter in _filters.entries) ...[ + Text( + filter.value.title, + ), + const SizedBox(height: 16), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (final entry in filter.value.options.entries) ...[ + PlatformToggleButton( + onChanged: (value) async { + await _onSelectFilter( + filter.key, + entry.key, + ); + setState(() {}); + }, + checked: widget.selectedFilters[filter.key]!.contains( + entry.key, + ), + text: entry.value, + ), + ] + ], + ), + const SizedBox(height: 16) + ], + ], + ), + ); + } +} diff --git a/lib/utils/extension_runtime.dart b/lib/utils/extension_runtime.dart index 5aa27829..1ad30218 100644 --- a/lib/utils/extension_runtime.dart +++ b/lib/utils/extension_runtime.dart @@ -305,9 +305,12 @@ class ExtensionRuntime { latest(page) { throw new Error("not implement latest"); } - search(kw, page, screening) { + search(kw, page, filter) { throw new Error("not implement search"); } + createFilter(filter){ + throw new Error("not implement createFilter"); + } detail(url) { throw new Error("not implement detail"); } @@ -403,11 +406,15 @@ class ExtensionRuntime { }); } - Future> search(String kw, int page) async { + Future> search( + String kw, + int page, { + Map>? filter, + }) async { return _runExtension(() async { final jsResult = await runtime.handlePromise( - await runtime - .evaluateAsync('stringify(()=>extenstion.search("$kw",$page))'), + await runtime.evaluateAsync( + 'stringify(()=>extenstion.search("$kw",$page,${filter == null ? null : jsonEncode(filter)}))'), ); List result = jsonDecode(jsResult.stringResult).map((e) { @@ -422,6 +429,30 @@ class ExtensionRuntime { }); } + Future> createFilter({ + Map>? filter, + }) async { + late String eval; + if (filter == null) { + eval = 'stringify(()=>extenstion.createFilter())'; + } else { + eval = + 'stringify(()=>extenstion.createFilter(JSON.parse(\'${jsonEncode(filter)}\')))'; + } + return _runExtension(() async { + final jsResult = await runtime.handlePromise( + await runtime.evaluateAsync(eval), + ); + Map result = jsonDecode(jsResult.stringResult); + return result.map( + (key, value) => MapEntry( + key, + ExtensionFilter.fromJson(value), + ), + ); + }); + } + Future detail(String url) async { return _runExtension(() async { final jsResult = await runtime.handlePromise( diff --git a/lib/widgets/button.dart b/lib/widgets/button.dart index 751df93a..6e07bafb 100644 --- a/lib/widgets/button.dart +++ b/lib/widgets/button.dart @@ -105,3 +105,46 @@ class PlatformIconButton extends StatelessWidget { ); } } + +class PlatformToggleButton extends fluent.StatelessWidget { + const PlatformToggleButton({ + Key? key, + required this.checked, + required this.onChanged, + required this.text, + }) : super(key: key); + + final bool checked; + final void Function(bool)? onChanged; + final String text; + + Widget _buildAndroid(BuildContext context) { + return TextButton( + onPressed: () => onChanged?.call(!checked), + child: Text( + text, + style: TextStyle( + color: checked + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurface, + ), + ), + ); + } + + Widget _buildDesktop(BuildContext context) { + return fluent.ToggleButton( + checked: checked, + onChanged: onChanged, + child: Text(text), + ); + } + + @override + Widget build(BuildContext context) { + return PlatformBuildWidget( + androidBuilder: _buildAndroid, + desktopBuilder: _buildDesktop, + ); + } +} From 18481a246e4a4003996fe5b114e06aecf5138715 Mon Sep 17 00:00:00 2001 From: MiaoMint <1981324730@qq.com> Date: Mon, 28 Aug 2023 09:54:08 +0800 Subject: [PATCH 2/2] Fix android filter --- lib/pages/search/pages/search_extension.dart | 23 ++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/lib/pages/search/pages/search_extension.dart b/lib/pages/search/pages/search_extension.dart index c2e991aa..1ff396ac 100644 --- a/lib/pages/search/pages/search_extension.dart +++ b/lib/pages/search/pages/search_extension.dart @@ -5,6 +5,7 @@ import 'package:fluent_ui/fluent_ui.dart' as fluent; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; +import 'package:get/get.dart'; import 'package:miru_app/models/index.dart'; import 'package:miru_app/router/router.dart'; import 'package:miru_app/utils/extension.dart'; @@ -125,21 +126,26 @@ class _SearchExtensionPageState extends fluent.State { showModalBottomSheet( context: context, builder: (context) => Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 16, + ), child: Row( children: [ TextButton( onPressed: () { - router.pop(); + Get.back(); }, child: Text("common.cancel".i18n), ), const Spacer(), FilledButton( onPressed: () { - router.pop(); + Get.back(); _easyRefreshController.callRefresh(); }, child: Text("common.confirm".i18n), @@ -147,7 +153,16 @@ class _SearchExtensionPageState extends fluent.State { ], ), ), - Expanded(child: fiterWidget) + const Divider(), + Expanded( + child: Padding( + padding: const EdgeInsets.only( + left: 16, + right: 16, + top: 16, + ), + child: fiterWidget, + )) ], ), );