feat: back is greyed while an import runs, so nothing cuts it off
A sheet can be busy (SheetScaffold.busy): its close button is off and greyed, and back does nothing until it ends. The Anki import's sheet is busy while it imports; before, its close button looked as always and did nothing. A studyset's screen, while it imports a PDF or a path, greys its back button and the path's steps and ignores back: leaving would close the lists' blocs and cut the import off. Importing a path now says so in its state, as a PDF's did. 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
e32a3129f6
commit
2283999ca9
9 changed files with 320 additions and 197 deletions
|
|
@ -120,6 +120,12 @@ class const SheetScaffold({
|
|||
/// can be acted on (an Undo, say); those of the screens below would sit
|
||||
/// behind its barrier. The sheet then always takes its largest height.
|
||||
final bool hasSnackBars = false,
|
||||
|
||||
/// Whether something runs that mustn't be cut off, as an import: the
|
||||
/// close button is off, greyed, and back does nothing until it ends. A
|
||||
/// sheet that can be busy is shown with `swipeable` false, as a bottom
|
||||
/// sheet swiped away closes whatever it holds says.
|
||||
final bool busy = false,
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
/// The widest a sheet grows as a dialog.
|
||||
|
|
@ -154,7 +160,15 @@ class const SheetScaffold({
|
|||
: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
inset(_Header(title: title, action: action, onClose: onClose)),
|
||||
inset(
|
||||
_Header(
|
||||
title: title,
|
||||
action: action,
|
||||
onClose: busy
|
||||
? null
|
||||
: onClose ?? () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
),
|
||||
// Loose, so a short body keeps the sheet short; tight at a fixed
|
||||
// height, so the footer sits at the bottom.
|
||||
if (!isDialog && heightFactor != null)
|
||||
|
|
@ -176,16 +190,19 @@ class const SheetScaffold({
|
|||
],
|
||||
],
|
||||
);
|
||||
final content = hasSnackBars
|
||||
? ScaffoldMessenger(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
// The sheet keeps clear of the keyboard itself.
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: column,
|
||||
),
|
||||
)
|
||||
: column;
|
||||
final content = PopScope(
|
||||
canPop: !busy,
|
||||
child: hasSnackBars
|
||||
? ScaffoldMessenger(
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
// The sheet keeps clear of the keyboard itself.
|
||||
resizeToAvoidBottomInset: false,
|
||||
body: column,
|
||||
),
|
||||
)
|
||||
: column,
|
||||
);
|
||||
if (isDialog) {
|
||||
// The dialog keeps clear of the window's edges and the keyboard
|
||||
// itself; insetting here as well would count them twice.
|
||||
|
|
@ -238,9 +255,10 @@ class const SheetScaffold({
|
|||
}
|
||||
}
|
||||
|
||||
/// The sheet's title and its close button. The button is there beside
|
||||
/// swiping the sheet away, which fails with wet fingers, gloves or a sheet
|
||||
/// taller than the window, and a dialog can't be swiped at all.
|
||||
/// The sheet's title and its close button, off without [onClose]. The
|
||||
/// button is there beside swiping the sheet away, which fails with wet
|
||||
/// fingers, gloves or a sheet taller than the window, and a dialog can't be
|
||||
/// swiped at all.
|
||||
class const _Header({
|
||||
required final String title,
|
||||
required final Widget? action,
|
||||
|
|
@ -261,7 +279,7 @@ class const _Header({
|
|||
IconButton(
|
||||
tooltip: AppLocalizations.of(context).commonClose,
|
||||
icon: const Icon(LucideIcons.x),
|
||||
onPressed: onClose ?? () => Navigator.of(context).maybePop(),
|
||||
onPressed: onClose,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
|
|
|||
|
|
@ -93,52 +93,48 @@ class const _AnkiImport() extends StatelessWidget {
|
|||
) => (summary, options),
|
||||
_ => (null, null),
|
||||
};
|
||||
return PopScope(
|
||||
canPop: !running,
|
||||
child: SheetScaffold(
|
||||
title: summary?.hasSubdecks ?? false
|
||||
? l10n.ankiImportOptionsTitle
|
||||
: l10n.ankiImportTitle,
|
||||
onClose: running ? () {} : null,
|
||||
footer: summary == null
|
||||
? null
|
||||
: FilledButton(
|
||||
onPressed: running
|
||||
? null
|
||||
: () => context.read<AnkiImportBloc>().add(
|
||||
const AnkiImportEventConfirmed(),
|
||||
),
|
||||
child: Text(l10n.ankiImportConfirm),
|
||||
),
|
||||
body: switch (state) {
|
||||
AnkiImportStateReading() || AnkiImportStateDone() => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||
child: Column(
|
||||
children: [
|
||||
const AppLoadingIndicator(),
|
||||
const SizedBox(height: 12),
|
||||
Text(l10n.ankiImportReading),
|
||||
],
|
||||
return SheetScaffold(
|
||||
title: summary?.hasSubdecks ?? false
|
||||
? l10n.ankiImportOptionsTitle
|
||||
: l10n.ankiImportTitle,
|
||||
// Closing would leave half the deck imported.
|
||||
busy: running,
|
||||
footer: summary == null
|
||||
? null
|
||||
: FilledButton(
|
||||
onPressed: running
|
||||
? null
|
||||
: () => context.read<AnkiImportBloc>().add(
|
||||
const AnkiImportEventConfirmed(),
|
||||
),
|
||||
child: Text(l10n.ankiImportConfirm),
|
||||
),
|
||||
body: switch (state) {
|
||||
AnkiImportStateReading() || AnkiImportStateDone() => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||
child: Column(
|
||||
children: [
|
||||
const AppLoadingIndicator(),
|
||||
const SizedBox(height: 12),
|
||||
Text(l10n.ankiImportReading),
|
||||
],
|
||||
),
|
||||
AnkiImportStateFailed(:final problem) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Text(switch (problem) {
|
||||
AnkiPackageProblem.notAPackage => l10n.ankiImportNotAPackage,
|
||||
AnkiPackageProblem.newerFormat => l10n.ankiImportNewerFormat,
|
||||
AnkiPackageProblem.unreadable => l10n.ankiImportUnreadable,
|
||||
null => l10n.ankiImportBrokeOff,
|
||||
}),
|
||||
),
|
||||
_ => _Options(
|
||||
summary: summary!,
|
||||
options: options!,
|
||||
progress: state is AnkiImportStateRunning
|
||||
? state.progress
|
||||
: null,
|
||||
),
|
||||
},
|
||||
),
|
||||
),
|
||||
AnkiImportStateFailed(:final problem) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Text(switch (problem) {
|
||||
AnkiPackageProblem.notAPackage => l10n.ankiImportNotAPackage,
|
||||
AnkiPackageProblem.newerFormat => l10n.ankiImportNewerFormat,
|
||||
AnkiPackageProblem.unreadable => l10n.ankiImportUnreadable,
|
||||
null => l10n.ankiImportBrokeOff,
|
||||
}),
|
||||
),
|
||||
_ => _Options(
|
||||
summary: summary!,
|
||||
options: options!,
|
||||
progress: state is AnkiImportStateRunning ? state.progress : null,
|
||||
),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ class PathListBloc(
|
|||
emit(state.copyWith(created: id));
|
||||
});
|
||||
on<PathListEventImportRequested>((event, emit) async {
|
||||
emit(state.copyWith(importing: true));
|
||||
try {
|
||||
final parsed = parsePathImport(event.source);
|
||||
final imported = await PathImporter(
|
||||
|
|
@ -37,6 +38,7 @@ class PathListBloc(
|
|||
).import(_studysetId, event.source);
|
||||
emit(
|
||||
state.copyWith(
|
||||
importing: false,
|
||||
importResult: (
|
||||
title: parsed.content.title,
|
||||
steps: imported.steps,
|
||||
|
|
@ -48,6 +50,7 @@ class PathListBloc(
|
|||
} on PathImportException catch (error) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
importing: false,
|
||||
importResult: (
|
||||
title: '',
|
||||
steps: 0,
|
||||
|
|
@ -207,6 +210,9 @@ final class const PathListState({
|
|||
/// null before any.
|
||||
final ImportResult? importResult,
|
||||
|
||||
/// Whether a path is being imported.
|
||||
final bool importing = false,
|
||||
|
||||
/// Whether the last read failed; the next change reads again.
|
||||
final bool failed = false,
|
||||
}) with Equatable {
|
||||
|
|
@ -228,6 +234,7 @@ final class const PathListState({
|
|||
bool? completionKnown,
|
||||
String? created,
|
||||
ImportResult? importResult,
|
||||
bool? importing,
|
||||
bool? failed,
|
||||
}) => PathListState(
|
||||
loaded: loaded ?? this.loaded,
|
||||
|
|
@ -237,6 +244,7 @@ final class const PathListState({
|
|||
completionKnown: completionKnown ?? this.completionKnown,
|
||||
created: created ?? this.created,
|
||||
importResult: importResult ?? this.importResult,
|
||||
importing: importing ?? this.importing,
|
||||
failed: failed ?? this.failed,
|
||||
);
|
||||
|
||||
|
|
@ -249,6 +257,7 @@ final class const PathListState({
|
|||
completionKnown,
|
||||
created,
|
||||
importResult,
|
||||
importing,
|
||||
failed,
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,10 @@ class const StudysetBreadcrumb({
|
|||
|
||||
/// What ends the line, after the path.
|
||||
final Widget? trailing,
|
||||
|
||||
/// Whether the steps lead anywhere: not while something runs that leaving
|
||||
/// would cut off, as an import.
|
||||
final bool enabled = true,
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
/// The room around a separator, on each side.
|
||||
|
|
@ -47,9 +51,15 @@ class const StudysetBreadcrumb({
|
|||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
final steps = <_Step>[
|
||||
_Step(l10n.libraryTitle, () => goUpTo(context, null, all)),
|
||||
_Step(
|
||||
l10n.libraryTitle,
|
||||
enabled ? () => goUpTo(context, null, all) : null,
|
||||
),
|
||||
for (final ancestor in ancestors)
|
||||
_Step(ancestor.content.title, () => goUpTo(context, ancestor.id, all)),
|
||||
_Step(
|
||||
ancestor.content.title,
|
||||
enabled ? () => goUpTo(context, ancestor.id, all) : null,
|
||||
),
|
||||
];
|
||||
Widget separator() => Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: _separatorPadding),
|
||||
|
|
@ -101,7 +111,9 @@ class const StudysetBreadcrumb({
|
|||
math.min(folded, steps.length),
|
||||
)) ...[
|
||||
MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
cursor: step.onTap == null
|
||||
? MouseCursor.defer
|
||||
: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: step.onTap,
|
||||
|
|
@ -140,8 +152,8 @@ class const StudysetBreadcrumb({
|
|||
}
|
||||
}
|
||||
|
||||
/// A step of the way, and where it leads.
|
||||
class const _Step(final String label, final VoidCallback onTap);
|
||||
/// A step of the way, and where it leads; nowhere while null.
|
||||
class const _Step(final String label, final VoidCallback? onTap);
|
||||
|
||||
/// The steps that didn't fit, behind a "…" that turns primary under the
|
||||
/// pointer, nearest first in its menu; a square target as high as a step.
|
||||
|
|
|
|||
|
|
@ -76,6 +76,10 @@ class const StudysetBackButton({
|
|||
|
||||
/// All the person's studysets, for [goUpTo].
|
||||
required final List<Studyset> all,
|
||||
|
||||
/// Whether it leads anywhere: not while something runs that leaving would
|
||||
/// cut off, as an import; it is greyed then.
|
||||
final bool enabled = true,
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
@override
|
||||
|
|
@ -94,7 +98,7 @@ class const StudysetBackButton({
|
|||
),
|
||||
],
|
||||
builder: (context, menu, _) => GestureDetector(
|
||||
onLongPress: menu.open,
|
||||
onLongPress: enabled ? menu.open : null,
|
||||
// The name shows under the pointer, but a long press is the list's:
|
||||
// a tooltip would take it on a touch screen.
|
||||
child: Tooltip(
|
||||
|
|
@ -102,7 +106,7 @@ class const StudysetBackButton({
|
|||
triggerMode: TooltipTriggerMode.manual,
|
||||
child: IconButton(
|
||||
icon: const BackButtonIcon(),
|
||||
onPressed: () => Navigator.maybePop(context),
|
||||
onPressed: enabled ? () => Navigator.maybePop(context) : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -339,150 +339,159 @@ class _StudysetViewState() extends State<_StudysetView> {
|
|||
final theme = Theme.of(context);
|
||||
final wide = context.windowSize.isAtLeastExpanded;
|
||||
final canGoBack = context.canPop();
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
appBar: AppBar(
|
||||
// Leaving would close the lists' blocs, and cut off what they import.
|
||||
final busy =
|
||||
context.select<PdfListBloc, bool>((bloc) => bloc.state.importing) ||
|
||||
context.select<PathListBloc, bool>((bloc) => bloc.state.importing);
|
||||
return PopScope(
|
||||
canPop: !busy,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
systemOverlayStyle: TopGradient.overlayStyle(context),
|
||||
leading: canGoBack
|
||||
? StudysetBackButton(
|
||||
ancestors: ancestorsOf(studyset.id, all),
|
||||
all: all,
|
||||
)
|
||||
: null,
|
||||
title: TitleWithKicker(
|
||||
title: studyset.content.title,
|
||||
kicker: studyset.content.secondaryTitle,
|
||||
titleMaxLines: 1,
|
||||
titleStyle:
|
||||
theme.appBarTheme.titleTextStyle ?? theme.textTheme.titleLarge,
|
||||
),
|
||||
actions: [
|
||||
if (wide) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: _StudyButton(studyset: studyset, inAppBar: true),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
systemOverlayStyle: TopGradient.overlayStyle(context),
|
||||
leading: canGoBack
|
||||
? StudysetBackButton(
|
||||
ancestors: ancestorsOf(studyset.id, all),
|
||||
all: all,
|
||||
enabled: !busy,
|
||||
)
|
||||
: null,
|
||||
title: TitleWithKicker(
|
||||
title: studyset.content.title,
|
||||
kicker: studyset.content.secondaryTitle,
|
||||
titleMaxLines: 1,
|
||||
titleStyle:
|
||||
theme.appBarTheme.titleTextStyle ?? theme.textTheme.titleLarge,
|
||||
),
|
||||
actions: [
|
||||
if (wide) ...[
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: _StudyButton(studyset: studyset, inAppBar: true),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: _AddMaterial(
|
||||
studysetId: studyset.id,
|
||||
controller: _addMaterial,
|
||||
builder: (context, menu) => roomy
|
||||
? FilledButton.tonalIcon(
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size(0, 48),
|
||||
),
|
||||
onPressed: () =>
|
||||
menu.isOpen ? menu.close() : menu.open(),
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
label: Text(
|
||||
AppLocalizations.of(context).studysetAddMaterial,
|
||||
),
|
||||
)
|
||||
: IconButton.filledTonal(
|
||||
tooltip: AppLocalizations.of(context)
|
||||
.studysetAddMaterial,
|
||||
onPressed: () =>
|
||||
menu.isOpen ? menu.close() : menu.open(),
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
Padding(
|
||||
padding: wide
|
||||
? const EdgeInsets.symmetric(horizontal: 8)
|
||||
: EdgeInsets.zero,
|
||||
child: ShareButton(studyset: studyset, all: all),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: _AddMaterial(
|
||||
studysetId: studyset.id,
|
||||
controller: _addMaterial,
|
||||
builder: (context, menu) => roomy
|
||||
? FilledButton.tonalIcon(
|
||||
style: FilledButton.styleFrom(
|
||||
minimumSize: const Size(0, 48),
|
||||
),
|
||||
onPressed: () =>
|
||||
menu.isOpen ? menu.close() : menu.open(),
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
label: Text(
|
||||
AppLocalizations.of(context).studysetAddMaterial,
|
||||
),
|
||||
)
|
||||
: IconButton.filledTonal(
|
||||
tooltip: AppLocalizations.of(context)
|
||||
.studysetAddMaterial,
|
||||
onPressed: () =>
|
||||
menu.isOpen ? menu.close() : menu.open(),
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
),
|
||||
),
|
||||
child: _Options(studyset: studyset, all: all),
|
||||
),
|
||||
],
|
||||
Padding(
|
||||
padding: wide
|
||||
? const EdgeInsets.symmetric(horizontal: 8)
|
||||
: EdgeInsets.zero,
|
||||
child: ShareButton(studyset: studyset, all: all),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: _Options(studyset: studyset, all: all),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: fab,
|
||||
// Above the licence line, which it would cover otherwise.
|
||||
floatingActionButtonLocation: wide
|
||||
? FloatingActionButtonLocation.endFloat
|
||||
: LicenseFooter.shows(studyset.content)
|
||||
? const _RaisedCenterFloat(_licenceClearance)
|
||||
: FloatingActionButtonLocation.centerFloat,
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
StudysetBreadcrumb(
|
||||
ancestors: ancestorsOf(studyset.id, all),
|
||||
current: studyset,
|
||||
all: all,
|
||||
trailing: wide
|
||||
? null
|
||||
: _TopicsButton(
|
||||
count: context.select<StudysetListBloc, int>(
|
||||
(bloc) => switch (bloc.state) {
|
||||
StudysetListStateLoaded(:final studysets) =>
|
||||
studysets.length,
|
||||
_ => 0,
|
||||
},
|
||||
),
|
||||
floatingActionButton: fab,
|
||||
// Above the licence line, which it would cover otherwise.
|
||||
floatingActionButtonLocation: wide
|
||||
? FloatingActionButtonLocation.endFloat
|
||||
: LicenseFooter.shows(studyset.content)
|
||||
? const _RaisedCenterFloat(_licenceClearance)
|
||||
: FloatingActionButtonLocation.centerFloat,
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
StudysetBreadcrumb(
|
||||
ancestors: ancestorsOf(studyset.id, all),
|
||||
current: studyset,
|
||||
all: all,
|
||||
enabled: !busy,
|
||||
trailing: wide
|
||||
? null
|
||||
: _TopicsButton(
|
||||
count: context.select<StudysetListBloc, int>(
|
||||
(bloc) => switch (bloc.state) {
|
||||
StudysetListStateLoaded(:final studysets) =>
|
||||
studysets.length,
|
||||
_ => 0,
|
||||
},
|
||||
),
|
||||
onPressed: () => _showTopics(context),
|
||||
),
|
||||
onPressed: () => _showTopics(context),
|
||||
),
|
||||
),
|
||||
CopyUpdateBanner(studyset: studyset),
|
||||
if (!wide && description != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
|
||||
child: SectionCard(child: description),
|
||||
),
|
||||
Expanded(
|
||||
child: wide
|
||||
? Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 4, 12, 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 420,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (description != null) ...[
|
||||
SectionCard(child: description),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
Expanded(
|
||||
child: SectionCard(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: rail,
|
||||
CopyUpdateBanner(studyset: studyset),
|
||||
if (!wide && description != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 12, 4),
|
||||
child: SectionCard(child: description),
|
||||
),
|
||||
Expanded(
|
||||
child: wide
|
||||
? Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 4, 12, 12),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 420,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (description != null) ...[
|
||||
SectionCard(child: description),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
Expanded(
|
||||
child: SectionCard(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: rail,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: SectionCard(
|
||||
padding: EdgeInsets.zero,
|
||||
child: content,
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: SectionCard(
|
||||
padding: EdgeInsets.zero,
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 4, 12, 12),
|
||||
child: SectionCard(
|
||||
padding: EdgeInsets.zero,
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 4, 12, 12),
|
||||
child: SectionCard(
|
||||
padding: EdgeInsets.zero,
|
||||
child: content,
|
||||
),
|
||||
),
|
||||
),
|
||||
LicenseFooter(content: studyset.content),
|
||||
],
|
||||
),
|
||||
LicenseFooter(content: studyset.content),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ void main() {
|
|||
required double width,
|
||||
Widget body = const Text('Body'),
|
||||
double statusBar = 0,
|
||||
bool busy = false,
|
||||
}) async {
|
||||
tester.view.physicalSize = Size(width, 900);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
|
|
@ -27,8 +28,10 @@ void main() {
|
|||
builder: (context) => TextButton(
|
||||
onPressed: () => showAdaptiveSheet<void>(
|
||||
context,
|
||||
swipeable: !busy,
|
||||
builder: (context) => SheetScaffold(
|
||||
title: 'New studyset',
|
||||
busy: busy,
|
||||
body: body,
|
||||
footer: FilledButton(
|
||||
onPressed: () {},
|
||||
|
|
@ -105,6 +108,27 @@ void main() {
|
|||
expect(find.text('New studyset'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('while busy, its close button is off and back keeps it open', (
|
||||
tester,
|
||||
) async {
|
||||
await open(tester, width: 400, busy: true);
|
||||
|
||||
expect(
|
||||
tester
|
||||
.widget<IconButton>(
|
||||
find.ancestor(
|
||||
of: find.byTooltip('Close'),
|
||||
matching: find.byType(IconButton),
|
||||
),
|
||||
)
|
||||
.onPressed,
|
||||
isNull,
|
||||
);
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('New studyset'), findsOneWidget);
|
||||
});
|
||||
|
||||
group('a body longer than the sheet', () {
|
||||
final tall = Column(
|
||||
children: [for (var i = 0; i < 40; i++) const Text('Line')],
|
||||
|
|
|
|||
|
|
@ -106,7 +106,9 @@ void main() {
|
|||
'{"type": "image", "src": "https://example.org/x.png"}]}]}}',
|
||||
),
|
||||
);
|
||||
await until((s) => s.importing);
|
||||
final imported = await until((s) => s.importResult != null);
|
||||
expect(imported.importing, isFalse);
|
||||
expect(imported.importResult, (
|
||||
title: 'Particles',
|
||||
steps: 1,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_navigation.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
void main() {
|
||||
Future<void> show(WidgetTester tester, {required bool enabled}) =>
|
||||
tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: StudysetBackButton(
|
||||
ancestors: const [],
|
||||
all: const [],
|
||||
enabled: enabled,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
IconButton button(WidgetTester tester) => tester.widget<IconButton>(
|
||||
find.descendant(
|
||||
of: find.byType(StudysetBackButton),
|
||||
matching: find.byType(IconButton),
|
||||
),
|
||||
);
|
||||
|
||||
testWidgets('while something runs here, back is greyed and its levels '
|
||||
'stay closed', (tester) async {
|
||||
await show(tester, enabled: false);
|
||||
|
||||
expect(button(tester).onPressed, isNull);
|
||||
await tester.longPress(find.byType(StudysetBackButton));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Library'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('otherwise back works and holding it lists the levels', (
|
||||
tester,
|
||||
) async {
|
||||
await show(tester, enabled: true);
|
||||
|
||||
expect(button(tester).onPressed, isNotNull);
|
||||
await tester.longPress(find.byType(StudysetBackButton));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Library'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue