fix(libraries): restore tvOS empty tab focus

close #1330
This commit is contained in:
edde746
2026-06-14 00:05:44 +02:00
parent f3cb508649
commit 656cf1158b
5 changed files with 158 additions and 7 deletions
+3 -3
View File
@@ -251,7 +251,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
final tabState = _getTabState(tabController.index);
if (tabState != null) {
(tabState as dynamic).focusFirstItem();
(tabState as dynamic).focusContentOrChrome();
} else {
// State not available yet, retry after another frame
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -266,7 +266,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
void _focusCurrentTabImmediate() {
final tabState = _getTabState(tabController.index);
if (tabState != null) {
(tabState as dynamic).focusFirstItem();
(tabState as dynamic).focusContentOrChrome();
}
}
@@ -296,7 +296,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
if (_visibleTabs[tabController.index] == LibraryTabType.browse) {
(tabState as dynamic).focusChipsBar();
} else {
(tabState as dynamic).focusFirstItem();
(tabState as dynamic).focusContentOrChrome();
}
}
});
@@ -79,6 +79,7 @@ abstract class BaseLibraryTabState<T, W extends BaseLibraryTab<T>> extends State
bool _hasLoadedData = false;
@protected
bool hasFocused = false;
bool _hasFocusedChromeFallback = false;
// Getters for subclasses
List<T> get items => _items;
@@ -125,6 +126,7 @@ abstract class BaseLibraryTabState<T, W extends BaseLibraryTab<T>> extends State
if (oldWidget.library.globalKey != widget.library.globalKey) {
// Reset focus state for new library
hasFocused = false;
_hasFocusedChromeFallback = false;
_hasLoadedData = false;
// Immediately clear stale data before async load
_items = [];
@@ -169,14 +171,47 @@ abstract class BaseLibraryTabState<T, W extends BaseLibraryTab<T>> extends State
// from interfering with TabBarView page animations
if (!InputModeTracker.isKeyboardMode(context)) return;
if (widget.isActive && _hasLoadedData && !hasFocused && _items.isNotEmpty) {
if (!widget.isActive || !_hasLoadedData) return;
if (hasFocusableContent) {
_hasFocusedChromeFallback = false;
if (hasFocused) return;
hasFocused = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
focusFirstItem();
}
});
return;
}
if (!_hasFocusedChromeFallback) {
_hasFocusedChromeFallback = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
focusEmptyState();
}
});
}
}
/// Whether [focusFirstItem] has a real content target to focus.
@protected
bool get hasFocusableContent => _items.isNotEmpty;
/// Focus content when available, otherwise return to the library chrome.
void focusContentOrChrome() {
if (hasFocusableContent) {
focusFirstItem();
} else {
focusEmptyState();
}
}
/// Fallback for empty/error states, where content has no focusable child.
@protected
void focusEmptyState() {
widget.onBack?.call();
}
/// Focus the first item in the tab. Subclasses should override this.
@@ -357,10 +357,10 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
// On mobile (touch mode), skip auto-focus to prevent ensureVisible()
// from interfering with TabBarView page animations
if (!InputModeTracker.isKeyboardMode(context)) return;
if (widget.isActive && hasLoadedData && !hasFocused && loadedItems.isNotEmpty) {
if (widget.isActive && hasLoadedData && !hasFocused) {
hasFocused = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) focusFirstItem();
if (mounted) focusContentOrChrome();
});
}
}
@@ -423,6 +423,18 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
}
}
@override
bool get hasFocusableContent => _selectedGrouping == 'folders' || loadedItems.isNotEmpty;
@override
void focusContentOrChrome() {
if (hasFocusableContent) {
focusFirstItem();
} else {
focusChipsBar();
}
}
/// Height of the chips bar (padding + chip + padding)
static const double _chipsBarHeight = 32.0;
@@ -255,7 +255,20 @@ class _LibraryRecommendedTabState extends BaseLibraryTabState<MediaHub, LibraryR
@override
void focusFirstItem() {
if (PlatformDetector.isTV()) {
_tvBrowseRailKey.currentState?.requestFocus();
final rail = _tvBrowseRailKey.currentState;
if (rail != null) {
rail.requestFocus();
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
final rail = _tvBrowseRailKey.currentState;
if (rail != null) {
rail.requestFocus();
} else {
focusEmptyState();
}
});
return;
}
if (_hubKeys.isNotEmpty && items.isNotEmpty) {