fix(ui): app-bar tab chip strips scroll instead of overflowing

Shared TabChipStrip replaces the plain Rows in libraries/downloads/live
TV app bars (and live TV's hand-rolled scroll wrapper); chips center
themselves on focus so d-pad reaches off-screen tabs.
This commit is contained in:
edde746
2026-07-06 12:17:03 +02:00
parent 214a7c13dc
commit adf732a1a1
5 changed files with 33 additions and 8 deletions
+5
View File
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../utils/scroll_utils.dart';
import 'dpad_navigator.dart';
import 'key_event_utils.dart';
@@ -86,6 +87,10 @@ mixin FocusableChipStateMixin<T extends StatefulWidget> on State<T> {
void _onFocusChange() {
if (mounted) {
setState(() => _isFocused = focusNode.hasFocus);
// Same convention as FocusableTileStateMixin: a chip inside a
// scrollable strip (TabChipStrip, filter bars) reveals itself on
// focus; a no-op when no ancestor scrollable exists.
if (focusNode.hasFocus) scrollContextToCenter(context);
}
}
+2 -1
View File
@@ -12,6 +12,7 @@ import '../../services/music/music_playback_service.dart';
import '../../theme/mono_tokens.dart';
import '../../utils/music_navigation.dart';
import '../../widgets/app_icon.dart';
import '../../widgets/focusable_tab_chip.dart';
import '../../widgets/music/mini_player.dart';
import '../../widgets/music/track_row.dart';
import '../../services/settings_service.dart';
@@ -114,7 +115,7 @@ class DownloadsScreenState extends State<DownloadsScreen>
Widget _buildAppBarTitle() {
// On desktop/TV with side nav, show tabs in app bar
if (PlatformDetector.shouldUseSideNavigation(context)) {
return Row(
return TabChipStrip(
children: [
_buildTabChip(t.downloads.manage, 0),
const SizedBox(width: 8),
+2 -2
View File
@@ -24,6 +24,7 @@ import '../../utils/content_utils.dart';
import '../../widgets/app_menu.dart';
import '../../widgets/backend_badge.dart';
import '../../widgets/desktop_app_bar.dart';
import '../../widgets/focusable_tab_chip.dart';
import '../../widgets/library_management_sheet.dart';
import '../../services/storage_service.dart';
import '../../mixins/refreshable.dart';
@@ -681,8 +682,7 @@ class _LibrariesScreenState extends State<LibrariesScreen>
// On desktop/TV with side nav, show tabs in app bar (library name is in side nav)
if (PlatformDetector.shouldUseSideNavigation(context)) {
return Row(
mainAxisSize: .min,
return TabChipStrip(
children: [
for (int i = 0; i < _visibleTabs.length; i++) ...[
if (i > 0) const SizedBox(width: 8),
+3 -5
View File
@@ -23,6 +23,7 @@ import '../../utils/desktop_window_padding.dart';
import '../../utils/platform_detector.dart';
import '../../utils/snackbar_helper.dart';
import '../../widgets/app_icon.dart';
import '../../widgets/focusable_tab_chip.dart';
import '../../widgets/overlay_sheet.dart';
import 'reorder_favorites_sheet.dart';
import 'tabs/guide_tab.dart';
@@ -549,7 +550,7 @@ class _LiveTvScreenState extends State<LiveTvScreen>
final isRecordings = _currentTab == LiveTvTab.recordings;
return Scaffold(
appBar: AppBar(
title: useSideNav ? Row(children: _buildTabChipItems()) : Text(t.liveTv.title),
title: useSideNav ? TabChipStrip(children: _buildTabChipItems()) : Text(t.liveTv.title),
actions: DesktopAppBarHelper.buildAdjustedActions([
FocusableActionBar(
key: _actionBarKey,
@@ -646,10 +647,7 @@ class _LiveTvScreenState extends State<LiveTvScreen>
Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
alignment: .centerLeft,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(children: _buildTabChipItems()),
),
child: TabChipStrip(children: _buildTabChipItems()),
),
Expanded(
child: TabBarView(
+21
View File
@@ -5,6 +5,27 @@ import '../focus/input_mode_tracker.dart';
import '../utils/platform_detector.dart';
import 'focus_builders.dart';
/// Horizontally scrollable host for a row of [FocusableTabChip]s.
///
/// App-bar titles and header rows give the strip a bounded width; a plain
/// Row overflows it on narrow windows (visible as the striped overflow
/// indicator). The strip shrink-wraps like `mainAxisSize: min` and scrolls
/// instead. D-pad stays correct: chips center themselves on focus via the
/// chip mixin, so LEFT/RIGHT reaches off-screen tabs.
class TabChipStrip extends StatelessWidget {
final List<Widget> children;
const TabChipStrip({super.key, required this.children});
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(mainAxisSize: .min, children: children),
);
}
}
/// A focusable tab chip that shows a color change when focused or selected.
///
/// Used for tab navigation in LibrariesScreen. Handles: