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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user