From 212f05a01d3a054f7e69f795159aff34356884c9 Mon Sep 17 00:00:00 2001 From: Julian Dice <19397727+windoze95@users.noreply.github.com> Date: Mon, 29 Jun 2026 06:31:57 -0500 Subject: [PATCH] =?UTF-8?q?feat(web):=20responsive=20desktop=20UX=20?= =?UTF-8?q?=E2=80=94=20NavigationRail=20+=20wider=20grids=20on=20big=20scr?= =?UTF-8?q?eens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make the app feel native on desktop/web (and iPad landscape) instead of showing a phone bottom-bar on a wide window. - Wide viewports (>=900px) get a side NavigationRail; phones keep the bottom nav. Both drive the same destinations list, so the web-hidden Offline tab is consistent across them. - gridCrossAxisCount scales with width (4 cols >=1150px, 5 >=1500px) instead of capping at 3, so library/discover grids use wide space. analyze clean, 156 tests pass, flutter build web succeeds. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/config/routes.dart | 30 ++++++++++++++++++++++++++++++ lib/widgets/adaptive_layout.dart | 11 +++++++++++ 2 files changed, 41 insertions(+) diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 6f53ebf..178349b 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -14,6 +14,8 @@ import '../screens/channel_detail_screen.dart'; import '../screens/video_player_screen.dart'; import '../screens/search_screen.dart'; import '../screens/queue_screen.dart'; +import '../widgets/adaptive_layout.dart'; +import 'theme.dart'; final _rootNavigatorKey = GlobalKey(); final _shellNavigatorKey = GlobalKey(); @@ -161,6 +163,34 @@ class _ScaffoldWithNav extends ConsumerWidget { final destinations = _destinations; final selectedIndex = _calculateSelectedIndex(context); + // Wide viewports (desktop/web, iPad landscape) get a side NavigationRail; + // phones keep the bottom nav. + if (AdaptiveLayout.isWide(context)) { + return Scaffold( + body: Row( + children: [ + NavigationRail( + selectedIndex: selectedIndex, + onDestinationSelected: (index) => + context.go(destinations[index].route), + labelType: NavigationRailLabelType.all, + backgroundColor: NullFeedTheme.surfaceColor, + destinations: [ + for (final d in destinations) + NavigationRailDestination( + icon: Icon(d.icon), + selectedIcon: Icon(d.activeIcon), + label: Text(d.label), + ), + ], + ), + const VerticalDivider(width: 1), + Expanded(child: child), + ], + ), + ); + } + return Scaffold( body: child, bottomNavigationBar: BottomNavigationBar( diff --git a/lib/widgets/adaptive_layout.dart b/lib/widgets/adaptive_layout.dart index 5ce420e..7792684 100644 --- a/lib/widgets/adaptive_layout.dart +++ b/lib/widgets/adaptive_layout.dart @@ -31,7 +31,18 @@ class AdaptiveLayout extends StatelessWidget { }; } + /// Breakpoint at/above which a side NavigationRail replaces the bottom nav + /// (desktop browsers, large windows, iPad landscape). + static const double wideBreakpoint = 900; + + static bool isWide(BuildContext context) => + MediaQuery.sizeOf(context).width >= wideBreakpoint; + static int gridCrossAxisCount(BuildContext context) { + // Make use of wide desktop/web viewports instead of capping at 3. + final width = MediaQuery.sizeOf(context).width; + if (width >= 1500) return 5; + if (width >= 1150) return 4; return switch (getDeviceType(context)) { DeviceType.tablet => 3, DeviceType.phone => 2,