fix(ui): allow landscape only in the full player

Keep application pages in portrait and let player sheets inherit rotation from the full player. Restore portrait on navigation, replacement, removal, and completed dismissal; discard stale minimization geometry when the screen rotates on close.

Validated with 12 Flutter tests and targeted analysis. iOS simulator build confirms portrait startup from a landscape device. Platform orientation restrictions still apply on large Android displays and multitasking iPads.
This commit is contained in:
zarzet
2026-09-25 18:05:40 +07:00
parent 1d53609b51
commit 2ab4aaa8dc
7 changed files with 280 additions and 2 deletions
+156
View File
@@ -0,0 +1,156 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:spotiflac_android/screens/now_playing_screen.dart';
import 'package:spotiflac_android/services/app_orientation.dart';
class _PlayerRoute extends NowPlayingRoute {
@override
Widget buildPage(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) => const SizedBox.expand();
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const portrait = ['DeviceOrientation.portraitUp'];
const player = [
'DeviceOrientation.portraitUp',
'DeviceOrientation.landscapeLeft',
'DeviceOrientation.landscapeRight',
];
final requests = <List<String>>[];
final messenger =
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
setUp(() {
requests.clear();
messenger.setMockMethodCallHandler(SystemChannels.platform, (call) async {
if (call.method == 'SystemChrome.setPreferredOrientations') {
requests.add((call.arguments as List<dynamic>).cast<String>());
}
return null;
});
});
tearDown(
() => messenger.setMockMethodCallHandler(SystemChannels.platform, null),
);
Future<NavigatorState> pumpApp(WidgetTester tester) async {
final key = GlobalKey<NavigatorState>();
await tester.pumpWidget(
MaterialApp(
navigatorKey: key,
navigatorObservers: [AppOrientationObserver()],
home: const Scaffold(body: Text('Library')),
),
);
await tester.pumpAndSettle();
expect(requests, [portrait]);
return key.currentState!;
}
test('startup requests portrait before the first route is built', () async {
await setAppOrientation();
expect(requests, [portrait]);
});
testWidgets('only the full player enables landscape; sheets inherit it', (
tester,
) async {
final navigator = await pumpApp(tester);
navigator.push(_PlayerRoute());
await tester.pumpAndSettle();
expect(requests, [portrait, player]);
showModalBottomSheet<void>(
context: navigator.context,
builder: (_) =>
const SizedBox(height: 100, child: Text('Player options')),
);
await tester.pumpAndSettle();
expect(requests, [portrait, player]);
navigator.pop();
await tester.pumpAndSettle();
expect(requests, [portrait, player]);
navigator.push(
MaterialPageRoute<void>(
builder: (_) => const Scaffold(body: Text('Album')),
),
);
await tester.pumpAndSettle();
expect(requests, [portrait, player, portrait]);
navigator.pop();
await tester.pumpAndSettle();
expect(requests, [portrait, player, portrait, player]);
await tester.binding.handlePopRoute();
await tester.pumpAndSettle();
expect(requests, [portrait, player, portrait, player, portrait]);
expect(tester.takeException(), isNull);
});
testWidgets('cancelled dismissal keeps rotation; completed drag locks it', (
tester,
) async {
final navigator = await pumpApp(tester);
final route = _PlayerRoute();
navigator.push(route);
await tester.pumpAndSettle();
route.startDrag();
route.updateDrag(
DragUpdateDetails(
globalPosition: const Offset(0, 240),
delta: const Offset(0, 240),
primaryDelta: 240,
),
600,
);
await tester.pump();
route.cancelDrag();
await tester.pumpAndSettle();
expect(requests, [portrait, player]);
route.startDrag();
route.endDrag(
DragEndDetails(
primaryVelocity: 1200,
velocity: const Velocity(pixelsPerSecond: Offset(0, 1200)),
),
600,
);
await tester.pumpAndSettle();
expect(requests, [portrait, player, portrait]);
expect(tester.takeException(), isNull);
});
testWidgets('removing and replacing routes cannot leave landscape enabled', (
tester,
) async {
final navigator = await pumpApp(tester);
final firstPlayer = _PlayerRoute();
navigator.push(firstPlayer);
await tester.pumpAndSettle();
final secondPlayer = _PlayerRoute();
navigator.push(secondPlayer);
await tester.pumpAndSettle();
navigator.removeRoute(firstPlayer);
await tester.pumpAndSettle();
expect(requests, [portrait, player]);
navigator.pushReplacement(
MaterialPageRoute<void>(
builder: (_) => const Scaffold(body: Text('Settings')),
),
);
await tester.pumpAndSettle();
expect(requests, [portrait, player, portrait]);
final thirdPlayer = _PlayerRoute();
navigator.push(thirdPlayer);
await tester.pumpAndSettle();
navigator.removeRoute(thirdPlayer);
await tester.pumpAndSettle();
expect(requests, [portrait, player, portrait, player, portrait]);
expect(tester.takeException(), isNull);
});
}
+39
View File
@@ -12,6 +12,8 @@ import 'package:spotiflac_android/theme/mornye_theme.dart';
const _sheet = Key('player-sheet');
class _TestPlayerRoute extends NowPlayingRoute {
_TestPlayerRoute({super.miniPlayerGeometry});
@override
Widget buildPage(
BuildContext context,
@@ -21,6 +23,43 @@ class _TestPlayerRoute extends NowPlayingRoute {
}
void main() {
testWidgets('rotation on close discards stale mini-player bounds', (
tester,
) async {
tester.view.physicalSize = const Size(852, 393);
tester.view.devicePixelRatio = 1;
addTearDown(tester.view.reset);
final navigator = GlobalKey<NavigatorState>();
await tester.pumpWidget(
MaterialApp(
navigatorKey: navigator,
theme: MornyeTheme.build(Brightness.dark),
home: const SizedBox(),
),
);
navigator.currentState!.push(
_TestPlayerRoute(
miniPlayerGeometry: () => (
surface: const Rect.fromLTWH(80, 310, 700, 52),
artwork: const Rect.fromLTWH(88, 314, 44, 44),
),
),
);
await tester.pumpAndSettle();
navigator.currentState!.pop();
await tester.pump();
expect(
find.byKey(const ValueKey('player-minimize-surface')),
findsOneWidget,
);
tester.view.physicalSize = const Size(393, 852);
await tester.pump(const Duration(milliseconds: 16));
expect(find.byKey(const ValueKey('player-minimize-surface')), findsNothing);
await tester.pumpAndSettle();
expect(find.byKey(_sheet), findsNothing);
expect(tester.takeException(), isNull);
});
for (final brightness in Brightness.values) {
testWidgets(
'player fills safe areas and reveals the page only when dragged in $brightness',