fix(learn): Settings no longer jumps while scrolled up slowly
Settings built its cards as they scrolled into view and let go of those far above. Scrolled back up, a card built anew asked again for what it shows and grew once answered: Japanese on this device stays hidden until the instance says what it offers devices, and Jellyfin's form fills in the server the instance suggests and looks it up. Whatever was on the screen was pushed down by as much, and near the top the list put right where it had thought the first cards were. Settings is short, so it is built whole now (`SingleChildScrollView`): nothing is let go of on the way, text typed into Jellyfin's form stays when scrolled away, and the scrollbar knows the list's length. The new test scrolls a phone's Settings up from the bottom, 20 pixels at a time, and holds every card to its place in the list; before, a card moved 64 pixels. The citation test now scrolls to the dropdown it taps, not to the guide's link below it, which only left the dropdown on the screen while the cards beneath weren't built yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
4e33133733
commit
c38dc69018
2 changed files with 106 additions and 47 deletions
|
|
@ -40,56 +40,64 @@ class const SettingsScreen({super.key}) extends StatelessWidget {
|
|||
constraints: const BoxConstraints(
|
||||
maxWidth: readableContentMaxWidth,
|
||||
),
|
||||
child: ListView(
|
||||
// Built whole, not as it scrolls into view (decided
|
||||
// 2026-10-25): a card built anew on the way back up asks again
|
||||
// for what it shows (the dictionaries the instance offers, the
|
||||
// Jellyfin server it suggests) and grows once answered, which
|
||||
// pushed everything on the screen down. Settings is short.
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
children: [
|
||||
const SyncStatusCard(),
|
||||
const SizedBox(height: 12),
|
||||
const DeviceSettingsCard(),
|
||||
const SizedBox(height: 12),
|
||||
const CitationSettingsCard(),
|
||||
const SizedBox(height: 12),
|
||||
const _StudyCard(),
|
||||
const SizedBox(height: 12),
|
||||
const AssistantSettingsCard(),
|
||||
const SizedBox(height: 12),
|
||||
const JellyfinSettingsCard(),
|
||||
const SizedBox(height: 12),
|
||||
const DeviceDictionarySettingsCard(),
|
||||
const _PrivacyCard(),
|
||||
const SizedBox(height: 12),
|
||||
SectionCard(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
|
||||
child: Text(
|
||||
l10n.settingsAccount,
|
||||
style: theme.textTheme.titleMedium,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SyncStatusCard(),
|
||||
const SizedBox(height: 12),
|
||||
const DeviceSettingsCard(),
|
||||
const SizedBox(height: 12),
|
||||
const CitationSettingsCard(),
|
||||
const SizedBox(height: 12),
|
||||
const _StudyCard(),
|
||||
const SizedBox(height: 12),
|
||||
const AssistantSettingsCard(),
|
||||
const SizedBox(height: 12),
|
||||
const JellyfinSettingsCard(),
|
||||
const SizedBox(height: 12),
|
||||
const DeviceDictionarySettingsCard(),
|
||||
const _PrivacyCard(),
|
||||
const SizedBox(height: 12),
|
||||
SectionCard(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 4),
|
||||
child: Text(
|
||||
l10n.settingsAccount,
|
||||
style: theme.textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Which account, on which instance (decided
|
||||
// 2026-10-16), switching, signing out and deleting it:
|
||||
// halkyon_app's, in Learn's words where only Learn can
|
||||
// say it.
|
||||
AccountTiles(deleteMessage: l10n.settingsDeleteMessage),
|
||||
],
|
||||
// Which account, on which instance (decided
|
||||
// 2026-10-16), switching, signing out and deleting it:
|
||||
// halkyon_app's, in Learn's words where only Learn can
|
||||
// say it.
|
||||
AccountTiles(deleteMessage: l10n.settingsDeleteMessage),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
SectionCard(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ListTile(
|
||||
leading: const Icon(LucideIcons.info),
|
||||
title: Text(l10n.settingsAbout),
|
||||
onTap: () => showAbout(context),
|
||||
const SizedBox(height: 12),
|
||||
SectionCard(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: ListTile(
|
||||
leading: const Icon(LucideIcons.info),
|
||||
title: Text(l10n.settingsAbout),
|
||||
onTap: () => showAbout(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const _Version(),
|
||||
],
|
||||
const SizedBox(height: 24),
|
||||
const _Version(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import 'dart:io';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/citation/model/citation.dart';
|
||||
|
|
@ -7,6 +8,7 @@ import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/version/app_version.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/dictionary/model/device_dictionary.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/settings/data/settings_repository.dart';
|
||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
import '../../../support/fake_device_dictionaries.dart';
|
||||
|
|
@ -54,6 +56,55 @@ void main() {
|
|||
expect(find.text('Version 1.2.3 (build 45)'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('scrolled up slowly from the bottom, the cards move only as '
|
||||
'far as the screen scrolls, though some grow once the instance '
|
||||
'answers', (tester) async {
|
||||
tester.view.physicalSize = const Size(400, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
await pumpApp(
|
||||
tester,
|
||||
testDependencies(
|
||||
// Shown only once the instance said what it offers devices.
|
||||
deviceDictionaries: deviceDictionariesIn(
|
||||
FakeDeviceDictionaryGateway(offered: offeredJapanese()),
|
||||
),
|
||||
),
|
||||
location: RoutePaths.settings,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
final position = tester
|
||||
.state<ScrollableState>(find.byType(Scrollable).first)
|
||||
.position;
|
||||
position.jumpTo(position.maxScrollExtent);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Where each card built stands in the list: its place on the screen
|
||||
// plus how far the list is scrolled, which stays as long as nothing
|
||||
// above it changes its height.
|
||||
Map<Element, double> places() => {
|
||||
for (final card in find.byType(SectionCard).evaluate())
|
||||
card:
|
||||
tester.getTopLeft(find.byElementPredicate((e) => e == card)).dy +
|
||||
position.pixels,
|
||||
};
|
||||
while (position.pixels > 0) {
|
||||
final before = places();
|
||||
position.jumpTo(math.max(0, position.pixels - 20));
|
||||
await tester.pumpAndSettle();
|
||||
final after = places();
|
||||
for (final MapEntry(key: card, value: place) in before.entries) {
|
||||
if (after[card] case final now?) {
|
||||
expect(
|
||||
now,
|
||||
moreOrLessEquals(place),
|
||||
reason: 'a card moved at ${position.pixels}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
group('Japanese on this device', () {
|
||||
/// Opens Settings, tall enough to show every card at once.
|
||||
Future<void> openSettings(
|
||||
|
|
@ -161,7 +212,7 @@ void main() {
|
|||
location: RoutePaths.settings,
|
||||
);
|
||||
await tester.scrollUntilVisible(
|
||||
find.text('Official guide'),
|
||||
find.text('APA (7th edition)'),
|
||||
200,
|
||||
scrollable: find.byType(Scrollable).first,
|
||||
);
|
||||
|
|
|
|||
Loading…
Reference in a new issue