merge: 修正标题与观看全集跳转
This commit is contained in:
commit
5fce65ac57
6 files changed
+82
-52
No files matched your search
@@ -1194,3 +1194,9 @@ $ flutter test
|
|||||||
- 推荐流拖动使用独立 opaque 手势与 pointer 兜底,end/cancel 同时恢复浮层和轨道。
|
- 推荐流拖动使用独立 opaque 手势与 pointer 兜底,end/cancel 同时恢复浮层和轨道。
|
||||||
- 推荐浮层改为 AnimatedOpacity + IgnorePointer,进度条节点始终留在树中;scrub setState 均加 mounted 守卫。
|
- 推荐浮层改为 AnimatedOpacity + IgnorePointer,进度条节点始终留在树中;scrub setState 均加 mounted 守卫。
|
||||||
- `flutter test test/player/seek_scrub_visibility_test.dart`:6 项测试全部通过。
|
- `flutter test test/player/seek_scrub_visibility_test.dart`:6 项测试全部通过。
|
||||||
|
|
||||||
|
## 2026-10-22 推荐页播放跳转修正
|
||||||
|
|
||||||
|
- 标题、剧名行和观看全集统一通过 `playDramaFromCatalog` 打开非 `homeMode` 逐集播放页。
|
||||||
|
- 仅含集数的剧集标题不再显示在标题上方,集数只保留下方进度文案。
|
||||||
|
- `flutter analyze`:No issues found。
|
||||||
@@ -50,38 +50,44 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
|
|||||||
final l10n = AppLocalizations.of(context)!;
|
final l10n = AppLocalizations.of(context)!;
|
||||||
final dramaTitle = localizedDramaTitle(l10n, widget.drama);
|
final dramaTitle = localizedDramaTitle(l10n, widget.drama);
|
||||||
final episodeTitle = widget.episode.title.trim();
|
final episodeTitle = widget.episode.title.trim();
|
||||||
final headline = episodeTitle.isEmpty ? dramaTitle : episodeTitle;
|
final headline = _headline(l10n, dramaTitle, episodeTitle);
|
||||||
final synopsis = localizedDramaSynopsis(l10n, widget.drama).trim();
|
final synopsis = localizedDramaSynopsis(l10n, widget.drama).trim();
|
||||||
final genres = _genreValues();
|
final genres = _genreValues();
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
GestureDetector(
|
||||||
headline,
|
key: const Key('home-feed-title-open'),
|
||||||
key: const Key('home-feed-title'),
|
behavior: HitTestBehavior.opaque,
|
||||||
maxLines: 2,
|
onTap: widget.onOpenFullPlayer,
|
||||||
overflow: TextOverflow.ellipsis,
|
child: Text(
|
||||||
style: const TextStyle(
|
headline,
|
||||||
color: AppColors.textPrimary,
|
key: const Key('home-feed-title'),
|
||||||
fontSize: _headlineFont,
|
maxLines: 2,
|
||||||
fontWeight: FontWeight.w700,
|
overflow: TextOverflow.ellipsis,
|
||||||
height: 1.2,
|
style: const TextStyle(
|
||||||
letterSpacing: 0,
|
color: AppColors.textPrimary,
|
||||||
shadows: _textShadows,
|
fontSize: _headlineFont,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
height: 1.2,
|
||||||
|
letterSpacing: 0,
|
||||||
|
shadows: _textShadows,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
key: const Key('home-feed-series-name'),
|
key: const Key('home-feed-series-name'),
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: widget.onDetail,
|
onTap: widget.onOpenFullPlayer,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
dramaTitle,
|
dramaTitle,
|
||||||
|
key: const Key('home-feed-series-title'),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@@ -113,6 +119,23 @@ class _HomeFeedInfoPanelState extends State<HomeFeedInfoPanel> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _headline(
|
||||||
|
AppLocalizations l10n,
|
||||||
|
String dramaTitle,
|
||||||
|
String episodeTitle,
|
||||||
|
) {
|
||||||
|
if (episodeTitle.isEmpty) return dramaTitle;
|
||||||
|
final normalized = episodeTitle.toLowerCase().replaceAll(' ', '');
|
||||||
|
final localizedEpisode = l10n
|
||||||
|
.episodeLabel(widget.episode.number)
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll(' ', '');
|
||||||
|
final episodeOnly = RegExp(r'^(第?\d+集|episode\d+)$').hasMatch(normalized);
|
||||||
|
return normalized == localizedEpisode || episodeOnly
|
||||||
|
? dramaTitle
|
||||||
|
: episodeTitle;
|
||||||
|
}
|
||||||
|
|
||||||
Widget _tagMetaRow(AppLocalizations l10n, List<String> genres) {
|
Widget _tagMetaRow(AppLocalizations l10n, List<String> genres) {
|
||||||
return Row(
|
return Row(
|
||||||
key: const Key('home-feed-tags'),
|
key: const Key('home-feed-tags'),
|
||||||
|
|||||||
@@ -171,11 +171,12 @@ class _SeekProgressBarState extends State<SeekProgressBar> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _finish(double fraction, Duration duration) async {
|
Future<void> _finish(double fraction, Duration duration) async {
|
||||||
if (widget.locked || !_dragging) return;
|
final startedAt = _dragStartFraction;
|
||||||
|
_dragStartFraction = null;
|
||||||
|
if (widget.locked || !_dragging || startedAt == null) return;
|
||||||
setState(() => _dragging = false);
|
setState(() => _dragging = false);
|
||||||
_pointerMoved = false;
|
_pointerMoved = false;
|
||||||
_pointerStart = null;
|
_pointerStart = null;
|
||||||
_dragStartFraction = null;
|
|
||||||
widget.thumbnailService?.cancelPending();
|
widget.thumbnailService?.cancelPending();
|
||||||
_previewFraction.value = null;
|
_previewFraction.value = null;
|
||||||
widget.onScrubEnd?.call();
|
widget.onScrubEnd?.call();
|
||||||
|
|||||||
@@ -6,15 +6,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
import '../../core/data/models/drama.dart';
|
import '../../core/data/models/drama.dart';
|
||||||
import '../../core/data/services/catalog_controller.dart';
|
|
||||||
import '../../core/data/services/category_controller.dart';
|
import '../../core/data/services/category_controller.dart';
|
||||||
import '../../core/data/services/download_controller.dart';
|
|
||||||
import '../../core/data/services/home_feed_controller.dart';
|
import '../../core/data/services/home_feed_controller.dart';
|
||||||
import '../../core/data/services/wallet_controller.dart';
|
|
||||||
import '../../core/navigation/fade_page_route.dart';
|
|
||||||
import '../../core/theme/app_colors.dart';
|
import '../../core/theme/app_colors.dart';
|
||||||
import '../../core/widgets/empty_state.dart';
|
import '../../core/widgets/empty_state.dart';
|
||||||
import '../../l10n/app_localizations.dart';
|
import '../../l10n/app_localizations.dart';
|
||||||
|
import '../player/open_player.dart';
|
||||||
import '../player/player_view.dart';
|
import '../player/player_view.dart';
|
||||||
import 'recommend_types.dart';
|
import 'recommend_types.dart';
|
||||||
import 'widgets/all_dramas_filter_grid.dart';
|
import 'widgets/all_dramas_filter_grid.dart';
|
||||||
@@ -187,30 +184,6 @@ class _RecommendPageState extends State<RecommendPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openDrama(Drama drama, {String? episodeId}) async {
|
Future<void> _openDrama(Drama drama, {String? episodeId}) async {
|
||||||
final catalog = context.read<CatalogController>();
|
await playDramaFromCatalog(context, drama, episodeId: episodeId);
|
||||||
final wallet = context.read<WalletController>();
|
|
||||||
final downloads = context.read<DownloadController>();
|
|
||||||
var playable = drama;
|
|
||||||
try {
|
|
||||||
playable = await catalog.requireDetail(drama);
|
|
||||||
} catch (_) {
|
|
||||||
// 卡片预览仍可直接进入播放器。
|
|
||||||
}
|
|
||||||
if (!mounted) return;
|
|
||||||
final episodeIndex = episodeId == null
|
|
||||||
? 0
|
|
||||||
: playable.episodes.indexWhere((episode) => episode.id == episodeId);
|
|
||||||
await Navigator.of(context).push<void>(
|
|
||||||
FadePageRoute(
|
|
||||||
builder: (_) => PlayerPage(
|
|
||||||
drama: playable,
|
|
||||||
initialIndex: episodeIndex < 0 ? 0 : episodeIndex,
|
|
||||||
homeMode: true,
|
|
||||||
feedDramas: [playable],
|
|
||||||
wallet: wallet,
|
|
||||||
downloads: downloads,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -282,7 +282,7 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(
|
expect(
|
||||||
tester.widget<PlayerPage>(find.byType(PlayerPage).last).homeMode,
|
tester.widget<PlayerPage>(find.byType(PlayerPage).last).homeMode,
|
||||||
isTrue,
|
isFalse,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -321,7 +321,7 @@ void main() {
|
|||||||
episode: const Episode(
|
episode: const Episode(
|
||||||
id: 'feed-episode',
|
id: 'feed-episode',
|
||||||
number: 2,
|
number: 2,
|
||||||
title: 'Ignored Episode Title',
|
title: 'Episode 2',
|
||||||
isFree: true,
|
isFree: true,
|
||||||
),
|
),
|
||||||
controller: null,
|
controller: null,
|
||||||
@@ -353,10 +353,11 @@ void main() {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
tester.widget<Text>(find.byKey(const Key('home-feed-title'))).data,
|
tester.widget<Text>(find.byKey(const Key('home-feed-title'))).data,
|
||||||
'Ignored Episode Title',
|
'Feed Drama',
|
||||||
|
);
|
||||||
|
final seriesTitle = tester.widget<Text>(
|
||||||
|
find.byKey(const Key('home-feed-series-title')),
|
||||||
);
|
);
|
||||||
expect(find.text('Feed Drama'), findsOneWidget);
|
|
||||||
final seriesTitle = tester.widget<Text>(find.text('Feed Drama'));
|
|
||||||
expect(seriesTitle.style?.fontSize, 19);
|
expect(seriesTitle.style?.fontSize, 19);
|
||||||
expect(seriesTitle.style?.fontWeight, FontWeight.w600);
|
expect(seriesTitle.style?.fontWeight, FontWeight.w600);
|
||||||
expect(seriesTitle.style?.color, AppColors.textPrimary);
|
expect(seriesTitle.style?.color, AppColors.textPrimary);
|
||||||
@@ -370,7 +371,9 @@ void main() {
|
|||||||
expect(seriesChevron.color, AppColors.textPrimary);
|
expect(seriesChevron.color, AppColors.textPrimary);
|
||||||
expect(
|
expect(
|
||||||
tester.getTopLeft(find.byIcon(Icons.chevron_right_rounded)).dx -
|
tester.getTopLeft(find.byIcon(Icons.chevron_right_rounded)).dx -
|
||||||
tester.getTopRight(find.text('Feed Drama')).dx,
|
tester
|
||||||
|
.getTopRight(find.byKey(const Key('home-feed-series-title')))
|
||||||
|
.dx,
|
||||||
4,
|
4,
|
||||||
);
|
);
|
||||||
expect(find.byKey(const Key('home-feed-info-card')), findsNothing);
|
expect(find.byKey(const Key('home-feed-info-card')), findsNothing);
|
||||||
|
|||||||
@@ -278,6 +278,30 @@ void main() {
|
|||||||
expect(find.byKey(const Key('all-dramas-grid')), findsOneWidget);
|
expect(find.byKey(const Key('all-dramas-grid')), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('should_open_standalone_player_from_title_and_watch_all', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await _pumpRecommend(tester);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('home-feed-title-open')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(
|
||||||
|
tester.widget<PlayerPage>(find.byType(PlayerPage).last).homeMode,
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
await tester.binding.handlePopRoute();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.byKey(const Key('recommend-page')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('recommend-tab-recommend')), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('home-feed-full-series')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(
|
||||||
|
tester.widget<PlayerPage>(find.byType(PlayerPage).last).homeMode,
|
||||||
|
isFalse,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('should_open_player_and_search_from_recommend_page', (
|
testWidgets('should_open_player_and_search_from_recommend_page', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
@@ -290,7 +314,7 @@ void main() {
|
|||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
final player = tester.widget<PlayerPage>(find.byType(PlayerPage).last);
|
final player = tester.widget<PlayerPage>(find.byType(PlayerPage).last);
|
||||||
expect(player.homeMode, isTrue);
|
expect(player.homeMode, isFalse);
|
||||||
await tester.binding.handlePopRoute();
|
await tester.binding.handlePopRoute();
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user