fix(map): a region is picked on a screen of its own, and a country's map offered once it is picked
The composer showed the map inside its scrolling page, so zooming and scrolling fought over the wheel. A map question's answer is now a field that opens the map in the whole window, as a studyset's description is written, with Save in the app bar; nothing scrolls there, so the wheel alone zooms. The Prefectures chip stood beside East Asia's countries before Japan was picked; it now shows once Japan, or a prefecture, is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MMm7usAo6q9ZZRoPkWjoxi Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
43bf1d2987
commit
3181223940
12 changed files with 327 additions and 110 deletions
|
|
@ -925,6 +925,7 @@
|
|||
"mapUnknown": "Diese Landkarte braucht eine neuere Version der App.",
|
||||
"mapPickRegion": "Wähle einen Teil der Karte, dann tippe auf die Region, die die Frage beantwortet. Winzige Orte haben einen Ring; wo Ringe dicht liegen, zoome hinein.",
|
||||
"mapNonePicked": "Noch keine Region gewählt",
|
||||
"mapPickTitle": "Region wählen",
|
||||
"mapNameJapan": "Japan",
|
||||
"mapNameWorld": "Welt",
|
||||
"mapPrefectures": "Präfekturen",
|
||||
|
|
@ -951,6 +952,7 @@
|
|||
"mapGroupOceania": "Ozeanien",
|
||||
"composerPlaceholderQuestion": "Frage hinzufügen",
|
||||
"composerLabelRegion": "Antwort",
|
||||
"composerRegionPick": "Tippen, um die Region auf der Karte zu wählen",
|
||||
|
||||
"homeStreakTooltip": "Lernserie",
|
||||
"homeArrange": "Widgets anordnen",
|
||||
|
|
|
|||
|
|
@ -1030,6 +1030,7 @@
|
|||
"mapUnknown": "This map needs a newer version of the app.",
|
||||
"mapPickRegion": "Choose a part of the map, then tap the region that answers the question. Tiny places have a ring; where rings crowd, zoom in.",
|
||||
"mapNonePicked": "No region picked yet",
|
||||
"mapPickTitle": "Pick the region",
|
||||
"mapNameJapan": "Japan",
|
||||
"mapNameWorld": "World",
|
||||
"mapPrefectures": "Prefectures",
|
||||
|
|
@ -1056,6 +1057,7 @@
|
|||
"mapGroupOceania": "Oceania",
|
||||
"composerPlaceholderQuestion": "Add question",
|
||||
"composerLabelRegion": "Answer",
|
||||
"composerRegionPick": "Tap to pick the region on the map",
|
||||
|
||||
"homeStreakTooltip": "Study streak",
|
||||
"homeArrange": "Arrange widgets",
|
||||
|
|
|
|||
|
|
@ -5458,6 +5458,12 @@ abstract class AppLocalizations {
|
|||
/// **'No region picked yet'**
|
||||
String get mapNonePicked;
|
||||
|
||||
/// No description provided for @mapPickTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Pick the region'**
|
||||
String get mapPickTitle;
|
||||
|
||||
/// No description provided for @mapNameJapan.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
@ -5614,6 +5620,12 @@ abstract class AppLocalizations {
|
|||
/// **'Answer'**
|
||||
String get composerLabelRegion;
|
||||
|
||||
/// No description provided for @composerRegionPick.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tap to pick the region on the map'**
|
||||
String get composerRegionPick;
|
||||
|
||||
/// No description provided for @homeStreakTooltip.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -3419,6 +3419,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get mapNonePicked => 'Noch keine Region gewählt';
|
||||
|
||||
@override
|
||||
String get mapPickTitle => 'Region wählen';
|
||||
|
||||
@override
|
||||
String get mapNameJapan => 'Japan';
|
||||
|
||||
|
|
@ -3497,6 +3500,10 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get composerLabelRegion => 'Antwort';
|
||||
|
||||
@override
|
||||
String get composerRegionPick =>
|
||||
'Tippen, um die Region auf der Karte zu wählen';
|
||||
|
||||
@override
|
||||
String get homeStreakTooltip => 'Lernserie';
|
||||
|
||||
|
|
|
|||
|
|
@ -3393,6 +3393,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get mapNonePicked => 'No region picked yet';
|
||||
|
||||
@override
|
||||
String get mapPickTitle => 'Pick the region';
|
||||
|
||||
@override
|
||||
String get mapNameJapan => 'Japan';
|
||||
|
||||
|
|
@ -3471,6 +3474,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get composerLabelRegion => 'Answer';
|
||||
|
||||
@override
|
||||
String get composerRegionPick => 'Tap to pick the region on the map';
|
||||
|
||||
@override
|
||||
String get homeStreakTooltip => 'Study streak';
|
||||
|
||||
|
|
|
|||
65
halkyon_learn_flutter/lib/src/core/widget/chevron_field.dart
Normal file
65
halkyon_learn_flutter/lib/src/core/widget/chevron_field.dart
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// A form's field whose value is set on a screen of its own: it shows the
|
||||
/// [value], up to [maxLines] lines, or the [placeholder] in italics while
|
||||
/// there is none, and a chevron leads on, as tapping opens that screen.
|
||||
/// So the form never holds what needs a whole window (an editor, a map).
|
||||
class const ChevronField({
|
||||
/// What the field holds, above it.
|
||||
required final String label,
|
||||
|
||||
/// The value as text; null while there is none.
|
||||
required final String? value,
|
||||
|
||||
/// What shows while there is no value, inviting to set one.
|
||||
required final String placeholder,
|
||||
|
||||
/// Opens the screen that sets the value.
|
||||
required final VoidCallback onTap,
|
||||
final int maxLines = 3,
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colors = theme.colorScheme;
|
||||
return InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
border: const OutlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.fromLTRB(12, 12, 4, 12),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
value ?? placeholder,
|
||||
maxLines: maxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: value == null
|
||||
? colors.onSurfaceVariant
|
||||
: colors.onSurface,
|
||||
fontStyle: value == null ? FontStyle.italic : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
LucideIcons.chevronRight,
|
||||
size: 18,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/content/published_ref.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_view.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/chevron_field.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/bloc/card_composer_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/widget/composer/answers_editor.dart';
|
||||
|
|
@ -18,8 +19,8 @@ import 'package:halkyon_learn_flutter/src/feature/dictionary/widget/studyset_dic
|
|||
import 'package:halkyon_learn_flutter/src/feature/geo_map/data/geo_map_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/geo_map/model/geo_map.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/geo_map/widget/map_of.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/geo_map/widget/region_map.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/geo_map/widget/region_names.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/geo_map/widget/region_picker.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/tag/model/tag.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/tag/model/tagging.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/tag/widget/tag_chip.dart';
|
||||
|
|
@ -578,8 +579,9 @@ class _CardDraftEditorState() extends State<CardDraftEditor> {
|
|||
}
|
||||
}
|
||||
|
||||
/// A map question's answer: the region to point at, picked on the maps
|
||||
/// as one, from the layer the studyset's other map questions share.
|
||||
/// A map question's answer: the region to point at, as a field that opens
|
||||
/// the maps as one in the whole window ([pickRegion]), from the region
|
||||
/// picked or the layer the studyset's other map questions share.
|
||||
class const _MapAnswer({
|
||||
required final CardContent content,
|
||||
required final Set<String> around,
|
||||
|
|
@ -590,50 +592,20 @@ class const _MapAnswer({
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final picked = content.answers.firstOrNull?.region;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(l10n.composerLabelRegion, style: theme.textTheme.labelLarge),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
l10n.mapPickRegion,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
MapOf(
|
||||
builder: (context, atlas) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
RegionMap(
|
||||
atlas: atlas,
|
||||
selected: picked,
|
||||
around: around,
|
||||
onRegionTapped: (code) =>
|
||||
onPicked(atlas.mapOf(code)!, atlas.region(code)!),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
switch (atlas.region(picked)) {
|
||||
final region? => switch (regionOtherName(
|
||||
context,
|
||||
atlas.mapOf(picked)!,
|
||||
region,
|
||||
)) {
|
||||
final other? => '${regionName(context, region)} · $other',
|
||||
null => regionName(context, region),
|
||||
},
|
||||
null => l10n.mapNonePicked,
|
||||
},
|
||||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
return MapOf(
|
||||
builder: (context, atlas) => ChevronField(
|
||||
label: l10n.composerLabelRegion,
|
||||
value: pickedRegionName(context, atlas, picked),
|
||||
placeholder: l10n.composerRegionPick,
|
||||
maxLines: 1,
|
||||
onTap: () async {
|
||||
if (await pickRegion(context, initial: picked, around: around)
|
||||
case final code? when code != picked) {
|
||||
onPicked(atlas.mapOf(code)!, atlas.region(code)!);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@ import 'package:material_ui/material_ui.dart';
|
|||
/// What tapping a country with a map of its own (Japan: its prefectures)
|
||||
/// does.
|
||||
enum CountryTaps() {
|
||||
/// Picks the country, and a chip opens its map: while writing a
|
||||
/// question, which may be about either.
|
||||
/// Picks the country, and once it or a region within it is picked, a
|
||||
/// chip opens its map: while writing a question, which may be about
|
||||
/// either.
|
||||
pickOrOpen,
|
||||
|
||||
/// Picks the country: a question about the country.
|
||||
|
|
@ -33,7 +34,9 @@ enum CountryTaps() {
|
|||
/// its own, its prefectures, in groups too (Kantō, Kansai …), which opens
|
||||
/// from Japan as [countryTaps] says. Back goes up a layer. So a region one
|
||||
/// can hardly hit on a phone is picked where it is large enough: Cyprus in
|
||||
/// Europe rather than on the world, Tokyo in Kantō.
|
||||
/// Europe rather than on the world, Tokyo in Kantō. A country's own map is
|
||||
/// offered only once the country is picked, so the prefectures don't stand
|
||||
/// beside East Asia's other countries as if they were one.
|
||||
///
|
||||
/// Even there some are too small to hit (Liechtenstein, Malta, the
|
||||
/// Caribbean's islands): they get a ring, which picks them. A tap picks the
|
||||
|
|
@ -42,7 +45,7 @@ enum CountryTaps() {
|
|||
/// the least a finger is sure to hit) or outline within 8 dp. Where rings
|
||||
/// crowd, one zooms in, on any layer: pinching, or scrolling with Ctrl (⌘
|
||||
/// on a Mac) held, as maps on web pages do, so plain scrolling still scrolls
|
||||
/// the page; scrolling without it says so. Drags pan. In a page that
|
||||
/// the page; scrolling without it says so, unless [wheelZooms]. Drags pan. In a page that
|
||||
/// scrolls, the map is 4:3 but never taller than
|
||||
/// [RegionMapState.screenShare] of the screen, so all of it shows while
|
||||
/// zooming.
|
||||
|
|
@ -76,6 +79,10 @@ class const RegionMap({
|
|||
|
||||
/// The region picked wrongly, once checked.
|
||||
final String? wrong,
|
||||
|
||||
/// Whether the mouse's wheel zooms without Ctrl (or ⌘): where nothing
|
||||
/// around the map scrolls.
|
||||
final bool wheelZooms = false,
|
||||
super.key,
|
||||
}) extends StatefulWidget {
|
||||
@override
|
||||
|
|
@ -165,6 +172,16 @@ class RegionMapState()
|
|||
_MapGeometry _geometryOf(GeoMap map) =>
|
||||
_geometries[map] ??= _MapGeometry(map);
|
||||
|
||||
/// Whether the region picked is [country] or lies within it: only then
|
||||
/// does a chip offer the country's own map.
|
||||
bool _pickedIn(String country) => switch (widget.atlas.mapOf(
|
||||
widget.selected,
|
||||
)) {
|
||||
null => false,
|
||||
final map =>
|
||||
widget.selected == country || widget.atlas.countryOf(map) == country,
|
||||
};
|
||||
|
||||
/// Whether a tap picks a region here, rather than a group.
|
||||
bool get _picksRegions => _level.group != null || _level.map.groups.isEmpty;
|
||||
|
||||
|
|
@ -355,13 +372,14 @@ class RegionMapState()
|
|||
|
||||
/// A mouse's wheel with Ctrl (or ⌘) held zooms around the pointer, as
|
||||
/// pinching does, and the page around doesn't scroll; without, the page
|
||||
/// scrolls and a note says how to zoom.
|
||||
/// scrolls and a note says how to zoom. Where nothing around scrolls
|
||||
/// ([RegionMap.wheelZooms]), the wheel alone zooms.
|
||||
void _scrolled(PointerSignalEvent event) {
|
||||
if (event is! PointerScrollEvent || _view == null || _size == null) {
|
||||
return;
|
||||
}
|
||||
final keys = HardwareKeyboard.instance;
|
||||
if (!keys.isControlPressed && !keys.isMetaPressed) {
|
||||
if (!widget.wheelZooms && !keys.isControlPressed && !keys.isMetaPressed) {
|
||||
_noteZoomKeys();
|
||||
return;
|
||||
}
|
||||
|
|
@ -496,7 +514,8 @@ class RegionMapState()
|
|||
if (widget.countryTaps == CountryTaps.pickOrOpen &&
|
||||
_level.group != null)
|
||||
for (final MapEntry(key: country, value: map) in atlas.inside.entries)
|
||||
if (_level.group!.members.contains(country)) map,
|
||||
if (_level.group!.members.contains(country) && _pickedIn(country))
|
||||
map,
|
||||
],
|
||||
onBack: above == null ? null : () => _open(above),
|
||||
onChoose: _choose,
|
||||
|
|
@ -529,7 +548,8 @@ class RegionMapState()
|
|||
|
||||
/// Above the map: Back to the layer [above], if any; on a whole map a chip
|
||||
/// per group, which opens it; in a group its name, and a chip for each map
|
||||
/// within a country of it that one may [opens] (Japan's prefectures).
|
||||
/// within a country of it that one may [opens] (Japan's prefectures, once
|
||||
/// Japan is picked).
|
||||
class const _Header({
|
||||
required final MapLevel level,
|
||||
required final MapLevel? above,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/geo_map/model/atlas.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/geo_map/widget/map_of.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/geo_map/widget/region_map.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/geo_map/widget/region_names.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// Picks a region on the maps in the whole window, over what opened it, as
|
||||
/// a studyset's description is written: the map fills what the window
|
||||
/// leaves, so nothing around it scrolls and the wheel alone zooms. Opens on
|
||||
/// the region [initial], else around the regions [around]; completes with
|
||||
/// the code of the region picked once saved, or with null when left
|
||||
/// without saving.
|
||||
Future<String?> pickRegion(
|
||||
BuildContext context, {
|
||||
required String? initial,
|
||||
Set<String> around = const {},
|
||||
}) => Navigator.of(context).push<String>(
|
||||
MaterialPageRoute(
|
||||
fullscreenDialog: true,
|
||||
builder: (context) => _RegionPicker(initial: initial, around: around),
|
||||
),
|
||||
);
|
||||
|
||||
class const _RegionPicker({
|
||||
required final String? initial,
|
||||
required final Set<String> around,
|
||||
}) extends StatefulWidget {
|
||||
@override
|
||||
State<_RegionPicker> createState() => _RegionPickerState();
|
||||
}
|
||||
|
||||
class _RegionPickerState() extends State<_RegionPicker> {
|
||||
late var _picked = widget.initial;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.mapPickTitle),
|
||||
actions: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: FilledButton(
|
||||
onPressed: _picked == null
|
||||
? null
|
||||
: () => Navigator.of(context).pop(_picked),
|
||||
child: Text(l10n.commonSave),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: MapOf(
|
||||
builder: (context, atlas) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
l10n.mapPickRegion,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: RegionMap(
|
||||
atlas: atlas,
|
||||
selected: _picked,
|
||||
around: widget.around,
|
||||
wheelZooms: true,
|
||||
onRegionTapped: (code) => setState(() => _picked = code),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
pickedRegionName(context, atlas, _picked) ??
|
||||
l10n.mapNonePicked,
|
||||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// The region [code] by its name, with its other name after a dot where
|
||||
/// it has one (Tokyo · 東京都); null for none.
|
||||
String? pickedRegionName(BuildContext context, Atlas atlas, String? code) =>
|
||||
switch (atlas.region(code)) {
|
||||
final region? => switch (regionOtherName(
|
||||
context,
|
||||
atlas.mapOf(code)!,
|
||||
region,
|
||||
)) {
|
||||
final other? => '${regionName(context, region)} · $other',
|
||||
null => regionName(context, region),
|
||||
},
|
||||
null => null,
|
||||
};
|
||||
|
|
@ -6,6 +6,7 @@ import 'package:halkyon_learn_flutter/src/core/editor/rich_text_screen.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/l10n/content_languages.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/theme/palette_color.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/adaptive_sheet.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/chevron_field.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/confirm_dialog.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/field_label.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/palette_color_picker.dart';
|
||||
|
|
@ -590,48 +591,16 @@ class const _DescriptionField({
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final colors = theme.colorScheme;
|
||||
final text = description.plainText.trim();
|
||||
final empty = description.isBlank;
|
||||
return InputDecorator(
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.studysetSheetDescription,
|
||||
border: const OutlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.fromLTRB(12, 12, 4, 12),
|
||||
),
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
empty
|
||||
? l10n.studysetSheetDescriptionAdd
|
||||
: text.isNotEmpty
|
||||
? text
|
||||
: l10n.studysetSheetDescriptionAdded,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: empty ? colors.onSurfaceVariant : colors.onSurface,
|
||||
fontStyle: empty ? FontStyle.italic : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
LucideIcons.chevronRight,
|
||||
size: 18,
|
||||
color: colors.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
return ChevronField(
|
||||
label: l10n.studysetSheetDescription,
|
||||
value: description.isBlank
|
||||
? null
|
||||
: text.isNotEmpty
|
||||
? text
|
||||
: l10n.studysetSheetDescriptionAdded,
|
||||
placeholder: l10n.studysetSheetDescriptionAdd,
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import 'package:halkyon_learn_flutter/src/core/content/content_blocks.dart';
|
|||
import 'package:halkyon_learn_flutter/src/core/content/rich_content.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/editor/rich_content_editor.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/chevron_field.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_payload.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/card/model/card_content.dart';
|
||||
|
|
@ -122,6 +123,12 @@ void main() {
|
|||
await settle(tester);
|
||||
}
|
||||
|
||||
/// Opens the map from a map question's answer.
|
||||
Future<void> pickOnMap(WidgetTester tester) async {
|
||||
await tester.tap(find.byType(ChevronField));
|
||||
await settle(tester);
|
||||
}
|
||||
|
||||
/// Chooses [item] of the card's ⋮ menu.
|
||||
Future<void> action(WidgetTester tester, String item) async {
|
||||
await tester.tap(find.byTooltip('More actions'));
|
||||
|
|
@ -637,19 +644,32 @@ void main() {
|
|||
expect(saved.back, RichContent.empty());
|
||||
});
|
||||
|
||||
testWidgets('a map question takes the region tapped as its answer', (
|
||||
tester,
|
||||
) async {
|
||||
testWidgets('a map question takes the region tapped on a screen of its '
|
||||
'own, where nothing scrolls, as its answer once saved', (tester) async {
|
||||
await cards.create('japanese', card('Where is Tokyo?', 'east'));
|
||||
await showStudyset(tester, maps: await loadedMaps(tester));
|
||||
await open(tester, 'Where is Tokyo?');
|
||||
|
||||
await tester.tap(find.text('Map'));
|
||||
await settle(tester);
|
||||
expect(find.byType(RegionMap), findsNothing);
|
||||
await pickOnMap(tester);
|
||||
expect(find.text('No region picked yet'), findsOneWidget);
|
||||
expect(
|
||||
find.ancestor(
|
||||
of: find.byType(RegionMap),
|
||||
matching: find.byType(Scrollable),
|
||||
),
|
||||
findsNothing,
|
||||
);
|
||||
expect(tester.widget<RegionMap>(find.byType(RegionMap)).wheelZooms, true);
|
||||
tester.widget<RegionMap>(find.byType(RegionMap)).onRegionTapped!('JP-13');
|
||||
await settle(tester);
|
||||
expect(find.text('Tokyo · 東京都'), findsOneWidget);
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Save'));
|
||||
await settle(tester);
|
||||
expect(find.byType(RegionMap), findsNothing);
|
||||
expect(find.text('Tokyo · 東京都'), findsOneWidget);
|
||||
await tester.tap(find.byType(BackButton));
|
||||
await settle(tester);
|
||||
|
||||
|
|
@ -676,10 +696,13 @@ void main() {
|
|||
await tester.tap(find.text('Map'));
|
||||
await settle(tester);
|
||||
expect(find.text('Countries of the world'), findsNothing);
|
||||
await pickOnMap(tester);
|
||||
tester.widget<RegionMap>(find.byType(RegionMap)).onRegionTapped!('JP-13');
|
||||
await settle(tester);
|
||||
tester.widget<RegionMap>(find.byType(RegionMap)).onRegionTapped!('CY');
|
||||
await settle(tester);
|
||||
await tester.tap(find.widgetWithText(FilledButton, 'Save'));
|
||||
await settle(tester);
|
||||
// Beside the app's language, the studyset's: Japanese.
|
||||
expect(find.text('Cyprus · キプロス'), findsOneWidget);
|
||||
await tester.tap(find.byType(BackButton));
|
||||
|
|
@ -720,6 +743,7 @@ void main() {
|
|||
|
||||
await tester.tap(find.text('Map'));
|
||||
await settle(tester);
|
||||
await pickOnMap(tester);
|
||||
|
||||
expect(tester.widget<RegionMap>(find.byType(RegionMap)).around, {
|
||||
'JP-13',
|
||||
|
|
@ -731,4 +755,23 @@ void main() {
|
|||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('leaving the map without saving keeps the answer', (
|
||||
tester,
|
||||
) async {
|
||||
await cards.create('japanese', card('Where is Tokyo?', 'east'));
|
||||
await showStudyset(tester, maps: await loadedMaps(tester));
|
||||
await open(tester, 'Where is Tokyo?');
|
||||
await tester.tap(find.text('Map'));
|
||||
await settle(tester);
|
||||
|
||||
await pickOnMap(tester);
|
||||
tester.widget<RegionMap>(find.byType(RegionMap)).onRegionTapped!('JP-13');
|
||||
await settle(tester);
|
||||
await tester.tap(find.byType(CloseButton));
|
||||
await settle(tester);
|
||||
|
||||
expect(find.byType(RegionMap), findsNothing);
|
||||
expect(find.text('Tap to pick the region on the map'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ void main() {
|
|||
});
|
||||
|
||||
/// Shows the maps at a phone's size, anew unless [key] is the one shown;
|
||||
/// what is picked lands in the list.
|
||||
/// what is picked lands in the list, and shows as picked.
|
||||
Future<List<String>> show(
|
||||
WidgetTester tester, {
|
||||
Key? key,
|
||||
|
|
@ -34,6 +34,7 @@ void main() {
|
|||
..devicePixelRatio = 1;
|
||||
addTearDown(tester.view.reset);
|
||||
final picked = <String>[];
|
||||
final shown = key ?? UniqueKey();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
theme: appTheme(Brightness.light),
|
||||
|
|
@ -42,14 +43,19 @@ void main() {
|
|||
home: Scaffold(
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: RegionMap(
|
||||
key: key ?? UniqueKey(),
|
||||
atlas: atlas,
|
||||
countryTaps: countryTaps,
|
||||
around: around,
|
||||
right: right,
|
||||
wrong: wrong,
|
||||
onRegionTapped: readOnly ? null : picked.add,
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setState) => RegionMap(
|
||||
key: shown,
|
||||
atlas: atlas,
|
||||
countryTaps: countryTaps,
|
||||
around: around,
|
||||
selected: picked.lastOrNull,
|
||||
right: right,
|
||||
wrong: wrong,
|
||||
onRegionTapped: readOnly
|
||||
? null
|
||||
: (code) => setState(() => picked.add(code)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -96,11 +102,12 @@ void main() {
|
|||
|
||||
const tokyo = (139.69, 35.69);
|
||||
|
||||
testWidgets('Japan is picked in East Asia, and a chip opens its '
|
||||
'prefectures in layers; Back goes up one', (tester) async {
|
||||
testWidgets('Japan is picked in East Asia, and once it is, a chip opens '
|
||||
'its prefectures in layers; Back goes up one', (tester) async {
|
||||
final picked = await show(tester);
|
||||
|
||||
await choose(tester, 'East Asia');
|
||||
expect(chip('Prefectures'), findsNothing);
|
||||
await tapAt(tester, tokyo);
|
||||
expect(picked, ['JP']);
|
||||
|
||||
|
|
@ -130,6 +137,11 @@ void main() {
|
|||
await tester.tap(back('East Asia'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('East Asia'), findsOneWidget);
|
||||
// Osaka is within Japan; Seoul isn't.
|
||||
expect(chip('Prefectures'), findsOneWidget);
|
||||
await tapAt(tester, (126.98, 37.57));
|
||||
expect(picked.last, 'KR');
|
||||
expect(chip('Prefectures'), findsNothing);
|
||||
await tester.tap(back('World'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(chip('Europe'), findsOneWidget);
|
||||
|
|
|
|||
Loading…
Reference in a new issue