diff --git a/lib/widgets/music/mini_player.dart b/lib/widgets/music/mini_player.dart index 07de899f2..7f6412eb0 100644 --- a/lib/widgets/music/mini_player.dart +++ b/lib/widgets/music/mini_player.dart @@ -231,14 +231,20 @@ class _MusicMiniPlayerOverlayState extends State { final (bottomInset, startInset) = context.select( (c) => (c?.bottomInset ?? 0, c?.startInset ?? 0), ); - final bottom = 12 + (bottomInset > 0 ? bottomInset : MediaQuery.paddingOf(context).bottom); + final padding = MediaQuery.paddingOf(context); + final isLtr = Directionality.of(context) == TextDirection.ltr; + final safeStart = isLtr ? padding.left : padding.right; + final safeEnd = isLtr ? padding.right : padding.left; + // The measured rail already includes its leading safe padding. + final start = startInset > safeStart ? startInset : safeStart; + final bottom = 12 + (bottomInset > 0 ? bottomInset : padding.bottom); return Stack( children: [ AnimatedPositionedDirectional( duration: const Duration(milliseconds: 200), curve: Curves.easeOutCubic, - start: 12 + startInset, - end: 12, + start: 12 + start, + end: 12 + safeEnd, bottom: bottom, child: switcher, ), diff --git a/test/widgets/music/mini_player_test.dart b/test/widgets/music/mini_player_test.dart index 3a2b20935..dfe2d7cd7 100644 --- a/test/widgets/music/mini_player_test.dart +++ b/test/widgets/music/mini_player_test.dart @@ -162,6 +162,7 @@ void main() { ActiveProfileProvider? activeProfileProvider, DownloadProvider? downloadProvider, MiniPlayerInsetController? insets, + TextDirection textDirection = TextDirection.ltr, }) { addTearDown(service.dispose); addTearDown(observer.suppress.dispose); @@ -184,6 +185,7 @@ void main() { child: MaterialApp( theme: monoTheme(dark: true).copyWith(platform: platform), navigatorObservers: [?navigatorObserver], + builder: (context, child) => Directionality(textDirection: textDirection, child: child!), home: const Stack( children: [ SizedBox.expand(), @@ -239,6 +241,51 @@ void main() { expect(screen.height - rect.bottom, 12); }); + for (final direction in TextDirection.values) { + testWidgets('keeps landscape controls clear of system insets in ${direction.name}', (tester) async { + tester.view.devicePixelRatio = 1; + tester.view.physicalSize = const Size(900, 410); + tester.view.padding = const FakeViewPadding(left: 24, right: 48, bottom: 20); + addTearDown(tester.view.reset); + final service = _FakeMusicService(track: _track); + final observer = MusicUiRouteObserver(); + final insets = MiniPlayerInsetController()..setNavInsets(bottom: 0, start: 80); + addTearDown(insets.dispose); + + await tester.pumpWidget(wrap(service: service, observer: observer, insets: insets, textDirection: direction)); + await tester.pumpAndSettle(); + final card = find.byKey(const ValueKey('mini_player_card')); + var rect = tester.getRect(card); + final isLtr = direction == TextDirection.ltr; + expect(rect.left, 12 + (isLtr ? 80 : 24)); + expect(rect.right, 900 - 12 - (isLtr ? 48 : 80)); + expect(rect.bottom, 410 - 12 - 20); + + // Pushing content hides the rail, not the operating system's insets. + insets.setNavBarSuspended(true); + await tester.pumpAndSettle(); + rect = tester.getRect(card); + expect(rect.left, 12 + 24); + expect(rect.right, 900 - 12 - 48); + final next = find.ancestor( + of: find.byWidgetPredicate((widget) => widget is AppIcon && widget.icon == Symbols.skip_next_rounded), + matching: find.byType(IconButton), + ); + final target = tester.getRect(next); + expect(target.left, greaterThanOrEqualTo(24)); + expect(target.right, lessThanOrEqualTo(900 - 48)); + await tester.tap(next); + await tester.pump(); + expect(service.nextCalls, 1); + + insets.setNavBarSuspended(false); + await tester.pumpAndSettle(); + rect = tester.getRect(card); + expect(rect.left, 12 + (isLtr ? 80 : 24)); + expect(rect.right, 900 - 12 - (isLtr ? 48 : 80)); + }); + } + testWidgets('details control announces the current title and artist exactly once', (tester) async { final semantics = tester.ensureSemantics(); final service = _FakeMusicService(track: _track);