fix(ui): a window resized across 600 dp keeps every screen as it was
Crossing from compact to medium windows or back built screens anew, losing what they held: a study session started over, a card being written lost its text, a video began again. The frame around a screen over the tabs (FramedScreen) was added above it only from medium windows on, and the snack bars' width (FittedSnackBars) put a Theme above the app's navigator only there, so the screens below sat at another depth after the change. The snack bars now always keep their Theme, changing only the width in it, and the framed screen keeps its screen under a global key, which moves it whole between the two places. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
da51ef73cb
commit
96cbd912de
4 changed files with 93 additions and 12 deletions
|
|
@ -173,16 +173,19 @@ class const FittedSnackBars({required final Widget child, super.key})
|
|||
/// How wide a snack bar is at most, from medium windows on.
|
||||
static const maxWidth = 560.0;
|
||||
|
||||
/// The same widgets above [child] in every window, only the theme in
|
||||
/// them changing: a widget added or left out above it as the window
|
||||
/// crosses a size would build the app's navigator, and every screen in
|
||||
/// it, anew.
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!context.windowSize.isAtLeastMedium) {
|
||||
return child;
|
||||
}
|
||||
final theme = Theme.of(context);
|
||||
return Theme(
|
||||
data: theme.copyWith(
|
||||
snackBarTheme: theme.snackBarTheme.copyWith(width: maxWidth),
|
||||
),
|
||||
data: context.windowSize.isAtLeastMedium
|
||||
? theme.copyWith(
|
||||
snackBarTheme: theme.snackBarTheme.copyWith(width: maxWidth),
|
||||
)
|
||||
: theme,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -62,6 +62,11 @@ class const FramedScreen({required final Widget child, super.key})
|
|||
class _FramedScreenState() extends State<FramedScreen> {
|
||||
final _tint = ScreenTint();
|
||||
|
||||
/// Keeps the screen, and what it holds, as the frame comes and goes with
|
||||
/// the window's size: without, the screen would be built anew at a
|
||||
/// different depth (a study session started over).
|
||||
final _screen = GlobalKey();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_tint.dispose();
|
||||
|
|
@ -69,12 +74,15 @@ class _FramedScreenState() extends State<FramedScreen> {
|
|||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => context.windowSize.isAtLeastMedium
|
||||
? ScreenTintScope(
|
||||
notifier: _tint,
|
||||
child: TintFrame(tint: _tint, child: widget.child),
|
||||
)
|
||||
: widget.child;
|
||||
Widget build(BuildContext context) {
|
||||
final screen = KeyedSubtree(key: _screen, child: widget.child);
|
||||
return context.windowSize.isAtLeastMedium
|
||||
? ScreenTintScope(
|
||||
notifier: _tint,
|
||||
child: TintFrame(tint: _tint, child: screen),
|
||||
)
|
||||
: screen;
|
||||
}
|
||||
}
|
||||
|
||||
/// The screen: a rounded panel inset on the frame, beside the rail or
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/theme/app_theme.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/tinted_frame.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
void main() {
|
||||
/// A screen with something typed into it, as a study session or a card
|
||||
/// being written holds what was done.
|
||||
Future<void> typeIn(
|
||||
WidgetTester tester,
|
||||
Widget Function(Widget) around,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(1200, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: appTheme(Brightness.light),
|
||||
home: around(const Scaffold(body: TextField())),
|
||||
),
|
||||
);
|
||||
await tester.enterText(find.byType(TextField), 'kept');
|
||||
}
|
||||
|
||||
/// Crosses every line where the frame or the snack bars change: medium,
|
||||
/// compact, medium again.
|
||||
Future<void> resize(WidgetTester tester) async {
|
||||
for (final width in const [700.0, 400.0, 1200.0]) {
|
||||
tester.view.physicalSize = Size(width, 800);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
}
|
||||
|
||||
testWidgets('a framed screen keeps what it holds when the window crosses '
|
||||
'from medium to compact and back', (tester) async {
|
||||
await typeIn(tester, (screen) => FramedScreen(child: screen));
|
||||
await resize(tester);
|
||||
|
||||
expect(find.text('kept'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('so does every screen under the snack bars fitted to the '
|
||||
'window', (tester) async {
|
||||
await typeIn(tester, (screen) => FittedSnackBars(child: screen));
|
||||
await resize(tester);
|
||||
|
||||
expect(find.text('kept'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
|
@ -343,6 +343,27 @@ void main() {
|
|||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('resizing the window, across every size class, keeps the '
|
||||
'video where it is', (tester) async {
|
||||
await watch(tester);
|
||||
await playTo(tester, const Duration(minutes: 2));
|
||||
final playing = players.last;
|
||||
|
||||
for (final size in const [
|
||||
Size(800, 900),
|
||||
Size(500, 900),
|
||||
Size(1400, 900),
|
||||
]) {
|
||||
tester.view.physicalSize = size;
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
expect(players.opened, hasLength(1));
|
||||
expect(players.last, same(playing));
|
||||
expect(playing!.disposed, isFalse);
|
||||
expect(playing.state.value.position, const Duration(minutes: 2));
|
||||
});
|
||||
|
||||
testWidgets('on a phone the words are below the video', (tester) async {
|
||||
await watch(tester, size: const Size(412, 915));
|
||||
await playTo(tester, const Duration(seconds: 2));
|
||||
|
|
|
|||
Loading…
Reference in a new issue