feat(app): 剧场列表增加语种筛选
This commit is contained in:
1 parent
ade0cc3566
commit
30a5fa0776
5 files changed
+220
-4
No files matched your search
@@ -36,6 +36,7 @@ class DiscoverLogic extends ChangeNotifier {
|
||||
Duration searchDebounce = const Duration(milliseconds: 350);
|
||||
static const int searchPageSize = 20;
|
||||
List<Drama> searchResults = const [];
|
||||
String? selectedLanguage;
|
||||
int searchTotal = 0;
|
||||
bool searchLoading = false;
|
||||
String? searchError;
|
||||
@@ -87,8 +88,32 @@ class DiscoverLogic extends ChangeNotifier {
|
||||
|
||||
/// 搜索态展示后端结果,非搜索态展示剧场自身的可见短剧。
|
||||
List<Drama> visibleDramas(List<Drama> fallback) {
|
||||
if (!hasSearchQuery) return fallback;
|
||||
return searchResults;
|
||||
if (!hasSearchQuery) return _filterByLanguage(fallback);
|
||||
return _filterByLanguage(searchResults);
|
||||
}
|
||||
|
||||
/// 当前 feed 中出现过的语种,按首次出现顺序去重。
|
||||
List<String> languagesOf(List<Drama> dramas) {
|
||||
final languages = <String>[];
|
||||
for (final drama in dramas) {
|
||||
final language = drama.language;
|
||||
if (language.isNotEmpty && !languages.contains(language)) {
|
||||
languages.add(language);
|
||||
}
|
||||
}
|
||||
return languages;
|
||||
}
|
||||
|
||||
/// 按语种筛选;null 表示全部。
|
||||
void selectLanguage(String? code) {
|
||||
selectedLanguage = code;
|
||||
notify();
|
||||
}
|
||||
|
||||
List<Drama> _filterByLanguage(List<Drama> dramas) {
|
||||
final code = selectedLanguage;
|
||||
if (code == null) return dramas;
|
||||
return dramas.where((drama) => drama.language == code).toList();
|
||||
}
|
||||
|
||||
Future<void> retrySearch() async {
|
||||
|
||||
@@ -176,7 +176,12 @@ class _DiscoverPageState extends State<DiscoverPage> {
|
||||
child: _categories(l10n, categories),
|
||||
),
|
||||
),
|
||||
if (logic.resumeRecord != null && logic.resumeDrama(allDramas) != null)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 0, 12),
|
||||
child: _languages(l10n, allDramas),
|
||||
),
|
||||
), if (logic.resumeRecord != null && logic.resumeDrama(allDramas) != null)
|
||||
SliverToBoxAdapter(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 14),
|
||||
@@ -427,6 +432,69 @@ class _DiscoverPageState extends State<DiscoverPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _languages(AppLocalizations l10n, List<Drama> dramas) {
|
||||
final languages = logic.languagesOf(dramas);
|
||||
if (languages.isEmpty) return const SizedBox.shrink();
|
||||
final selected = logic.selectedLanguage;
|
||||
return SizedBox(
|
||||
key: const Key('discover-language-chips'),
|
||||
height: AppDimens.tapTarget,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14),
|
||||
scrollDirection: Axis.horizontal,
|
||||
children: [
|
||||
_languageChip(
|
||||
key: const Key('discover-language-all'),
|
||||
label: l10n.all,
|
||||
selected: selected == null,
|
||||
onTap: () => logic.selectLanguage(null),
|
||||
),
|
||||
for (final language in languages)
|
||||
_languageChip(
|
||||
key: Key('discover-language-$language'),
|
||||
label: _languageLabel(l10n, language),
|
||||
selected: selected == language,
|
||||
onTap: () => logic.selectLanguage(language),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _languageChip({
|
||||
required Key key,
|
||||
required String label,
|
||||
required bool selected,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: Center(
|
||||
child: ChoiceChip(
|
||||
key: key,
|
||||
label: Text(label),
|
||||
selected: selected,
|
||||
showCheckmark: false,
|
||||
onSelected: (_) => onTap(),
|
||||
selectedColor: AppColors.blue,
|
||||
backgroundColor: const Color(0xFF29272B),
|
||||
side: BorderSide.none,
|
||||
labelStyle: TextStyle(
|
||||
color: selected ? AppColors.onPrimary : AppColors.muted,
|
||||
fontSize: AppDimens.chipFont,
|
||||
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _languageLabel(AppLocalizations l10n, String code) {
|
||||
if (code == 'zh-CN' || code == 'zh') return l10n.languageChinese;
|
||||
if (code == 'id') return l10n.languageIndonesian;
|
||||
if (code == 'en') return l10n.languageEnglish;
|
||||
return code;
|
||||
}
|
||||
Widget _continueCard(
|
||||
AppLocalizations l10n,
|
||||
Drama drama,
|
||||
|
||||
@@ -13,6 +13,7 @@ void main() {
|
||||
'totalEpisodes': 77,
|
||||
'isHot': true,
|
||||
'viewCount': 3100000,
|
||||
'language': 'zh-CN',
|
||||
'preview': {
|
||||
'id': 9,
|
||||
'number': 1,
|
||||
@@ -27,6 +28,7 @@ void main() {
|
||||
expect(drama.episodes.single.videoUrl, '/files/episode/9.mp4');
|
||||
expect(drama.episodeCount, 77);
|
||||
expect(drama.isHot, isTrue);
|
||||
expect(drama.language, 'zh-CN');
|
||||
});
|
||||
|
||||
test('should_have_no_episodes_when_list_card_has_no_playable_preview', () {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
|
||||
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
|
||||
|
||||
import 'package:fruitshorts/app.dart';
|
||||
import 'package:fruitshorts/core/data/models/content_category.dart';
|
||||
import 'package:fruitshorts/core/data/models/drama.dart';
|
||||
import 'package:fruitshorts/core/data/models/home_feed.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/category_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
|
||||
|
||||
import '../support/fake_catalog.dart';
|
||||
import '../support/fake_interaction_repositories.dart';
|
||||
import '../support/fake_notice_repository.dart';
|
||||
import '../support/widget_player.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
SharedPreferencesAsyncPlatform.instance =
|
||||
InMemorySharedPreferencesAsync.empty();
|
||||
});
|
||||
|
||||
testWidgets('should_filter_theater_grid_when_language_chip_is_tapped', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(390, 844);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
await tester.pumpWidget(
|
||||
LumaDramaApp(
|
||||
playerFactory: createWidgetPlayer,
|
||||
catalogRepository: FakeCatalogRepository(),
|
||||
categoryRepository: _MixedLanguageCategoryRepository(),
|
||||
watchHistoryRepository: InMemoryWatchHistoryRepository(),
|
||||
favoriteRepository: InMemoryFavoriteRepository(),
|
||||
playbackRepository: FakePlaybackRepository(),
|
||||
commentRepository: FakeCommentRepository(),
|
||||
interactionRepository: FakeInteractionRepository(),
|
||||
noticeRepository: FakeNoticeRepository(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byKey(const Key('tab-discover')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('home-drama-card-zh-demo')), findsOneWidget);
|
||||
expect(find.byKey(const Key('home-drama-card-id-demo')), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byKey(const Key('discover-language-zh-CN')));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('home-drama-card-zh-demo')), findsOneWidget);
|
||||
expect(find.byKey(const Key('home-drama-card-id-demo')), findsNothing);
|
||||
});
|
||||
}
|
||||
|
||||
class _MixedLanguageCategoryRepository implements CategoryRepository {
|
||||
@override
|
||||
Future<List<ContentCategory>> fetchCategories() async => const [];
|
||||
|
||||
@override
|
||||
Future<HomeFeed> fetchDiscoverFeed() async => HomeFeed(
|
||||
sections: [
|
||||
FeedSection(
|
||||
code: 'hot',
|
||||
title: 'Hot',
|
||||
dramas: [
|
||||
_drama('zh-demo', 'Chinese Demo', 'zh-CN'),
|
||||
_drama('id-demo', 'Indonesian Demo', 'id'),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Drama _drama(String id, String title, String language) => Drama(
|
||||
id: id,
|
||||
title: title,
|
||||
synopsis: '$title synopsis',
|
||||
posterUrl: '/files/$id.png',
|
||||
language: language,
|
||||
detailLoaded: true,
|
||||
);
|
||||
@@ -8,7 +8,8 @@ import 'package:fruitshorts/pages/discover/discover_logic.dart';
|
||||
|
||||
import '../support/fake_watch_history.dart';
|
||||
|
||||
Drama _drama(String id, {List<int> numbers = const [1, 2, 3]}) => Drama(
|
||||
Drama _drama(String id, {List<int> numbers = const [1, 2, 3], String language = ''}) => Drama(
|
||||
language: language,
|
||||
id: id,
|
||||
title: id,
|
||||
synopsis: 'synopsis',
|
||||
@@ -64,6 +65,36 @@ void main() {
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('should_filter_visible_dramas_when_language_is_selected', () {
|
||||
final logic = DiscoverLogic(history: FakeWatchHistory(), drama: _drama('s'));
|
||||
final dramas = [
|
||||
_drama('zh', language: 'zh-CN'),
|
||||
_drama('id', language: 'id'),
|
||||
_drama('unknown'),
|
||||
];
|
||||
|
||||
expect(logic.visibleDramas(dramas), hasLength(3));
|
||||
|
||||
logic.selectLanguage('id');
|
||||
expect(logic.visibleDramas(dramas).map((item) => item.id), ['id']);
|
||||
|
||||
logic.selectLanguage(null);
|
||||
expect(logic.visibleDramas(dramas), hasLength(3));
|
||||
logic.dispose();
|
||||
});
|
||||
|
||||
test('should_list_distinct_languages_when_feed_has_cards', () {
|
||||
final logic = DiscoverLogic(history: FakeWatchHistory(), drama: _drama('s'));
|
||||
final dramas = [
|
||||
_drama('zh', language: 'zh-CN'),
|
||||
_drama('id', language: 'id'),
|
||||
_drama('id2', language: 'id'),
|
||||
_drama('unknown'),
|
||||
];
|
||||
|
||||
expect(logic.languagesOf(dramas), ['zh-CN', 'id']);
|
||||
logic.dispose();
|
||||
});
|
||||
test('should_have_no_resume_position_when_server_has_no_record', () async {
|
||||
final history = FakeWatchHistory();
|
||||
final logic = DiscoverLogic(
|
||||
|
||||
Reference in new issue
Block a user