fix: correct D-pad focus navigation order on media detail screen

This commit is contained in:
edde746
2026-02-18 03:30:04 +01:00
parent 408bf8577d
commit 83e04b64da
+38 -32
View File
@@ -98,6 +98,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
late final FocusNode _castFocusNode;
final ScrollController _castScrollController = ScrollController();
final _castSectionKey = GlobalKey();
final _seasonsSectionKey = GlobalKey();
String _toGlobalKey(String ratingKey, {String? serverId}) =>
'${serverId ?? widget.metadata.serverId ?? ''}:$ratingKey';
@@ -1039,19 +1040,19 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
final metadata = _fullMetadata ?? widget.metadata;
// For shows, go to seasons first
if (metadata.isShow && _seasons.isNotEmpty) {
_seasonsFocusNode.requestFocus();
return KeyEventResult.handled;
}
// For movies (or shows with no seasons): overview → cast → extras
// DOWN order: overview → seasons → cast → extras
if (metadata.summary != null) {
_overviewFocusNode.requestFocus();
_scrollSectionIntoView(_overviewSectionKey);
return KeyEventResult.handled;
}
if (metadata.isShow && _seasons.isNotEmpty) {
_seasonsFocusNode.requestFocus();
_scrollSectionIntoView(_seasonsSectionKey);
return KeyEventResult.handled;
}
if (metadata.role != null && metadata.role!.isNotEmpty) {
_castFocusNode.requestFocus();
_scrollSectionIntoView(_castSectionKey);
@@ -1145,20 +1146,23 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
return KeyEventResult.handled;
}
// UP: scroll to top and focus play button
// UP: overview → play button
if (key.isUpKey) {
_scrollController.animateTo(0, duration: const Duration(milliseconds: 200), curve: Curves.easeOut);
_playButtonFocusNode.requestFocus();
return KeyEventResult.handled;
}
// DOWN: overview → cast → extras (first that exists)
if (key.isDownKey) {
final metadata = _fullMetadata ?? widget.metadata;
if (metadata.summary != null) {
_overviewFocusNode.requestFocus();
_scrollSectionIntoView(_overviewSectionKey);
} else if (metadata.role != null && metadata.role!.isNotEmpty) {
} else {
_scrollController.animateTo(0, duration: const Duration(milliseconds: 200), curve: Curves.easeOut);
_playButtonFocusNode.requestFocus();
}
return KeyEventResult.handled;
}
// DOWN: cast → extras
if (key.isDownKey) {
final metadata = _fullMetadata ?? widget.metadata;
if (metadata.role != null && metadata.role!.isNotEmpty) {
_castFocusNode.requestFocus();
_scrollSectionIntoView(_castSectionKey);
} else if (_extras != null && _extras!.isNotEmpty) {
@@ -1179,20 +1183,19 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
final metadata = _fullMetadata ?? widget.metadata;
// UP: seasons (if show) or play button
// UP: always play button (overview is directly below play)
if (key.isUpKey) {
if (metadata.isShow && _seasons.isNotEmpty) {
_seasonsFocusNode.requestFocus();
} else {
_scrollController.animateTo(0, duration: const Duration(milliseconds: 200), curve: Curves.easeOut);
_playButtonFocusNode.requestFocus();
}
_scrollController.animateTo(0, duration: const Duration(milliseconds: 200), curve: Curves.easeOut);
_playButtonFocusNode.requestFocus();
return KeyEventResult.handled;
}
// DOWN: cast → extras → consume
// DOWN: seasons (if show) → cast → extras
if (key.isDownKey) {
if (metadata.role != null && metadata.role!.isNotEmpty) {
if (metadata.isShow && _seasons.isNotEmpty) {
_seasonsFocusNode.requestFocus();
_scrollSectionIntoView(_seasonsSectionKey);
} else if (metadata.role != null && metadata.role!.isNotEmpty) {
_castFocusNode.requestFocus();
_scrollSectionIntoView(_castSectionKey);
} else if (_extras != null && _extras!.isNotEmpty) {
@@ -1330,17 +1333,18 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
return KeyEventResult.handled;
}
// UP: cast → overview → seasons → play button (first that exists)
// UP: cast → seasons (if show) → overview → play button
if (key.isUpKey) {
final metadata = _fullMetadata ?? widget.metadata;
if (metadata.role != null && metadata.role!.isNotEmpty) {
_castFocusNode.requestFocus();
_scrollSectionIntoView(_castSectionKey);
} else if (metadata.isShow && _seasons.isNotEmpty) {
_seasonsFocusNode.requestFocus();
_scrollSectionIntoView(_seasonsSectionKey);
} else if (metadata.summary != null) {
_overviewFocusNode.requestFocus();
_scrollSectionIntoView(_overviewSectionKey);
} else if (_seasons.isNotEmpty) {
_seasonsFocusNode.requestFocus();
} else {
_scrollController.animateTo(0, duration: const Duration(milliseconds: 200), curve: Curves.easeOut);
_playButtonFocusNode.requestFocus();
@@ -1383,13 +1387,14 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
return KeyEventResult.handled;
}
// UP: overview → seasons (if show) → play button
// UP: seasons (if show) → overview → play button
if (key.isUpKey) {
if (metadata.summary != null) {
if (metadata.isShow && _seasons.isNotEmpty) {
_seasonsFocusNode.requestFocus();
_scrollSectionIntoView(_seasonsSectionKey);
} else if (metadata.summary != null) {
_overviewFocusNode.requestFocus();
_scrollSectionIntoView(_overviewSectionKey);
} else if (metadata.isShow && _seasons.isNotEmpty) {
_seasonsFocusNode.requestFocus();
} else {
_scrollController.animateTo(0, duration: const Duration(milliseconds: 200), curve: Curves.easeOut);
_playButtonFocusNode.requestFocus();
@@ -1960,6 +1965,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen> with WatchStateAw
// Seasons (for TV shows)
if (isShow) ...[
Text(
key: _seasonsSectionKey,
t.discover.seasons,
style: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold),
),