Files
minstrel/flutter_client/lib/shared/routing.dart
T
bvandeusen 59427bcf2f fix(flutter): full player unmounts the shell — no doubled player UI
You correctly diagnosed: the mini bar was still visible underneath
the full player, and that's also why the bottom controls overflowed
by 45px (the shell's mini bar was eating the bottom 88dp).

Move /now-playing OUT of the ShellRoute and up to a top-level GoRoute.
Now pushing /now-playing unmounts the shell entirely — no mini bar
underneath, no fight for the bottom strip. The slide-up transition
still feels right because the shell stays painted for the duration of
the animation (so during the transition both are briefly visible, as
you noted is fine).

Drop the hideMini conditional in _ShellWithPlayerBar — no longer
needed since the shell isn't even built when the full player is on
top. Recovering the 88dp also resolves the bottom-controls overflow
without needing to shrink the layout.
2026-05-11 08:09:53 -04:00

149 lines
6.0 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../auth/auth_provider.dart';
import '../auth/login_screen.dart';
import '../auth/server_url_screen.dart';
import '../library/album_detail_screen.dart';
import '../library/artist_detail_screen.dart';
import '../discover/discover_screen.dart';
import '../library/home_screen.dart';
import '../library/library_screen.dart';
import '../player/now_playing_screen.dart';
import '../player/player_bar.dart';
import '../player/queue_screen.dart';
import '../playlists/playlist_detail_screen.dart';
import '../playlists/playlists_list_screen.dart';
import '../requests/requests_screen.dart';
import '../search/search_screen.dart';
import '../settings/settings_screen.dart';
import '../update/client_update_provider.dart';
import '../update/update_banner.dart';
import '../admin/admin_landing_screen.dart';
import '../admin/admin_requests_screen.dart';
import '../admin/admin_quarantine_screen.dart';
import '../admin/admin_users_screen.dart';
import 'widgets/version_gate.dart';
/// Exposed as a Provider so its single argument is a real `Ref` (the
/// constructor's redirect closures use `ref.read`). Widgets consume the
/// router via `ref.watch(routerProvider)` instead of constructing it
/// themselves with a `WidgetRef`, which can't satisfy the `Ref` type.
final routerProvider = Provider<GoRouter>((ref) => buildRouter(ref));
GoRouter buildRouter(Ref ref) {
return GoRouter(
initialLocation: '/',
redirect: (ctx, state) async {
final url = await ref.read(serverUrlProvider.future);
if (url == null) return '/server-url';
final user = await ref.read(authControllerProvider.future);
final loc = state.matchedLocation;
if (user == null && loc != '/login' && loc != '/server-url') {
return '/login';
}
if (user != null && (loc == '/login' || loc == '/server-url')) {
return '/home';
}
if (loc.startsWith('/admin') && !user!.isAdmin) {
return '/home';
}
return null;
},
routes: [
GoRoute(path: '/', redirect: (_, __) => '/home'),
GoRoute(path: '/server-url', builder: (_, __) => const ServerUrlScreen()),
GoRoute(path: '/login', builder: (_, __) => const LoginScreen()),
// /now-playing lives outside the ShellRoute on purpose. The full
// player IS the player UI when active, and we don't want the mini
// bar from the shell underneath fighting for the bottom strip.
// Pushing this route unmounts the shell entirely; the slide-up
// transition still feels right because the shell stays painted
// for the duration of the animation.
GoRoute(
path: '/now-playing',
pageBuilder: (_, __) => CustomTransitionPage(
child: const NowPlayingScreen(),
transitionDuration: const Duration(milliseconds: 280),
reverseTransitionDuration: const Duration(milliseconds: 240),
transitionsBuilder: (_, anim, __, child) {
final eased = CurvedAnimation(
parent: anim,
curve: Curves.easeOutCubic,
reverseCurve: Curves.easeInCubic,
);
return SlideTransition(
position: Tween(
begin: const Offset(0, 1),
end: Offset.zero,
).animate(eased),
child: child,
);
},
),
),
ShellRoute(
builder: (ctx, state, child) => VersionGate(child: _ShellWithPlayerBar(child: child)),
routes: [
GoRoute(path: '/home', builder: (_, __) => const HomeScreen()),
GoRoute(
path: '/artists/:id',
builder: (_, s) => ArtistDetailScreen(id: s.pathParameters['id']!),
),
GoRoute(
path: '/albums/:id',
builder: (_, s) => AlbumDetailScreen(id: s.pathParameters['id']!),
),
GoRoute(path: '/queue', builder: (_, __) => const QueueScreen()),
GoRoute(path: '/search', builder: (_, __) => const SearchScreen()),
GoRoute(path: '/library', builder: (_, __) => const LibraryScreen()),
GoRoute(path: '/discover', builder: (_, __) => const DiscoverScreen()),
GoRoute(path: '/settings', builder: (_, __) => const SettingsScreen()),
GoRoute(path: '/playlists', builder: (_, __) => const PlaylistsListScreen()),
GoRoute(
path: '/playlists/:id',
builder: (_, s) => PlaylistDetailScreen(id: s.pathParameters['id']!),
),
GoRoute(path: '/requests', builder: (_, __) => const RequestsScreen()),
GoRoute(path: '/admin', builder: (_, __) => const AdminLandingScreen()),
GoRoute(path: '/admin/requests', builder: (_, __) => const AdminRequestsScreen()),
GoRoute(path: '/admin/quarantine', builder: (_, __) => const AdminQuarantineScreen()),
GoRoute(path: '/admin/users', builder: (_, __) => const AdminUsersScreen()),
],
),
],
);
}
class _ShellWithPlayerBar extends ConsumerWidget {
const _ShellWithPlayerBar({required this.child});
final Widget child;
@override
Widget build(BuildContext context, WidgetRef ref) {
// The banner sits above the routed child and uses SafeArea to clear
// the system status bar. MediaQuery.padding is screen-relative
// though, so the child's Scaffold/AppBar would still apply its own
// top status-bar inset on top of the banner — doubling the gap.
// When the banner is showing, strip that inset from the child so
// its AppBar lands directly under the banner.
final hasBanner = ref.watch(shouldShowUpdateBannerProvider) != null;
return Column(
children: [
const UpdateBanner(),
Expanded(
child: hasBanner
? MediaQuery.removePadding(
context: context,
removeTop: true,
child: child,
)
: child,
),
const PlayerBar(),
],
);
}
}