feat(study): limits switch on and off, step by 5, and take any number typed
Your calls (2026-10-11): a switch per limit, and steps of 5 with any number typed. - A daily limit (new cards, reviews; in a studyset's study options and the study settings) is a switch, "Limit new cards per day", and its number while on. Counting down to 0 for no limit had to be guessed, and a goal has nothing like it. Switched off and on again, the limit comes back as it was; the first time, at the defaults' 20 new cards, or 200 reviews as Anki starts them. Stored as before: 0 still stands for none. - The study goal counts in steps of 5 as the limits do (reviews: 25). Plus and minus go to the next multiple, so 12 steps to 15 or 10, never below the first step. - Tapping the number lets any other be typed, from 1 (CountStepper.onTyped). Pomodoro minutes and cards per round keep their own steps. 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
2283999ca9
commit
abddd8b9b0
13 changed files with 370 additions and 93 deletions
|
|
@ -883,6 +883,9 @@
|
|||
"studySortNewestFirst": "Neueste zuerst",
|
||||
"studyFewer": "Verringern",
|
||||
"studyMore": "Erhöhen",
|
||||
"countStepperType": "Zahl eingeben",
|
||||
"studyLimitNewPerDay": "Neue Karten pro Tag begrenzen",
|
||||
"studyLimitReviewsPerDay": "Wiederholungen pro Tag begrenzen",
|
||||
|
||||
"studysetMenuReset": "Fortschritt zurücksetzen",
|
||||
"studysetResetTitle": "Fortschritt zurücksetzen?",
|
||||
|
|
|
|||
|
|
@ -979,6 +979,9 @@
|
|||
"studySortNewestFirst": "Newest first",
|
||||
"studyFewer": "Decrease",
|
||||
"studyMore": "Increase",
|
||||
"countStepperType": "Type a number",
|
||||
"studyLimitNewPerDay": "Limit new cards per day",
|
||||
"studyLimitReviewsPerDay": "Limit reviews per day",
|
||||
|
||||
"studysetMenuReset": "Reset progress",
|
||||
"studysetResetTitle": "Reset progress?",
|
||||
|
|
|
|||
|
|
@ -5218,6 +5218,24 @@ abstract class AppLocalizations {
|
|||
/// **'Increase'**
|
||||
String get studyMore;
|
||||
|
||||
/// No description provided for @countStepperType.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Type a number'**
|
||||
String get countStepperType;
|
||||
|
||||
/// No description provided for @studyLimitNewPerDay.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Limit new cards per day'**
|
||||
String get studyLimitNewPerDay;
|
||||
|
||||
/// No description provided for @studyLimitReviewsPerDay.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Limit reviews per day'**
|
||||
String get studyLimitReviewsPerDay;
|
||||
|
||||
/// No description provided for @studysetMenuReset.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
|
|
|||
|
|
@ -3231,6 +3231,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||
@override
|
||||
String get studyMore => 'Erhöhen';
|
||||
|
||||
@override
|
||||
String get countStepperType => 'Zahl eingeben';
|
||||
|
||||
@override
|
||||
String get studyLimitNewPerDay => 'Neue Karten pro Tag begrenzen';
|
||||
|
||||
@override
|
||||
String get studyLimitReviewsPerDay => 'Wiederholungen pro Tag begrenzen';
|
||||
|
||||
@override
|
||||
String get studysetMenuReset => 'Fortschritt zurücksetzen';
|
||||
|
||||
|
|
|
|||
|
|
@ -3212,6 +3212,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||
@override
|
||||
String get studyMore => 'Increase';
|
||||
|
||||
@override
|
||||
String get countStepperType => 'Type a number';
|
||||
|
||||
@override
|
||||
String get studyLimitNewPerDay => 'Limit new cards per day';
|
||||
|
||||
@override
|
||||
String get studyLimitReviewsPerDay => 'Limit reviews per day';
|
||||
|
||||
@override
|
||||
String get studysetMenuReset => 'Reset progress';
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
|
@ -5,6 +8,10 @@ import 'package:material_ui/material_ui.dart';
|
|||
/// A number between a minus and a plus, the width of what holds it, in an
|
||||
/// outlined box: the number bold ([style], body text by default), [suffix]
|
||||
/// (" / day") lighter after it. A null action can't go further.
|
||||
///
|
||||
/// With [onTyped], tapping the number lets one type another in its place,
|
||||
/// such as 12 between steps of 5 ([stepUp], [stepDown]); it is handed over
|
||||
/// once confirmed or left, if it is a number from 1.
|
||||
class const CountStepper({
|
||||
required final String value,
|
||||
required final VoidCallback? onFewer,
|
||||
|
|
@ -15,13 +22,88 @@ class const CountStepper({
|
|||
|
||||
/// The number's style, before it is made bold.
|
||||
final TextStyle? style,
|
||||
|
||||
/// The number typed, from 1; none to type without.
|
||||
final ValueChanged<int>? onTyped,
|
||||
|
||||
/// The number to start typing over: [value] as a number.
|
||||
final int? number,
|
||||
super.key,
|
||||
}) extends StatelessWidget {
|
||||
}) extends StatefulWidget {
|
||||
/// The most digits a number typed has.
|
||||
static const maxDigits = 4;
|
||||
|
||||
@override
|
||||
State<CountStepper> createState() => _CountStepperState();
|
||||
}
|
||||
|
||||
class _CountStepperState() extends State<CountStepper> {
|
||||
var _typing = false;
|
||||
final _field = TextEditingController();
|
||||
final _focus = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focus.addListener(() {
|
||||
if (!_focus.hasFocus && _typing) {
|
||||
_confirm();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_field.dispose();
|
||||
_focus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _type() {
|
||||
_field.text = '${widget.number ?? ''}';
|
||||
_field.selection = TextSelection(
|
||||
baseOffset: 0,
|
||||
extentOffset: _field.text.length,
|
||||
);
|
||||
setState(() => _typing = true);
|
||||
_focus.requestFocus();
|
||||
}
|
||||
|
||||
void _confirm() {
|
||||
final typed = int.tryParse(_field.text);
|
||||
setState(() => _typing = false);
|
||||
if (typed != null && typed >= 1) {
|
||||
widget.onTyped?.call(typed);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final colors = theme.colorScheme;
|
||||
final numberStyle = (widget.style ?? theme.textTheme.bodyLarge)?.copyWith(
|
||||
color: colors.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
);
|
||||
final shown = Text.rich(
|
||||
TextSpan(
|
||||
text: widget.value,
|
||||
children: [
|
||||
if (widget.suffix.isNotEmpty)
|
||||
TextSpan(
|
||||
text: widget.suffix,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: numberStyle,
|
||||
);
|
||||
final typable = widget.onTyped != null;
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
|
|
@ -33,34 +115,48 @@ class const CountStepper({
|
|||
children: [
|
||||
IconButton(
|
||||
tooltip: l10n.studyFewer,
|
||||
onPressed: onFewer,
|
||||
onPressed: _typing ? null : widget.onFewer,
|
||||
icon: const Icon(LucideIcons.minus),
|
||||
),
|
||||
Expanded(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
text: value,
|
||||
children: [
|
||||
if (suffix.isNotEmpty)
|
||||
TextSpan(
|
||||
text: suffix,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colors.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
child: _typing
|
||||
? TextField(
|
||||
controller: _field,
|
||||
focusNode: _focus,
|
||||
textAlign: TextAlign.center,
|
||||
style: numberStyle,
|
||||
keyboardType: TextInputType.number,
|
||||
textInputAction: TextInputAction.done,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.digitsOnly,
|
||||
LengthLimitingTextInputFormatter(
|
||||
CountStepper.maxDigits,
|
||||
),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
border: InputBorder.none,
|
||||
hintText: l10n.countStepperType,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
style: (style ?? theme.textTheme.bodyLarge)?.copyWith(
|
||||
color: colors.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
onSubmitted: (_) => _focus.unfocus(),
|
||||
)
|
||||
: typable
|
||||
? Tooltip(
|
||||
message: l10n.countStepperType,
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
onTap: _type,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: shown,
|
||||
),
|
||||
),
|
||||
)
|
||||
: shown,
|
||||
),
|
||||
IconButton(
|
||||
tooltip: l10n.studyMore,
|
||||
onPressed: onMore,
|
||||
onPressed: _typing ? null : widget.onMore,
|
||||
icon: const Icon(LucideIcons.plus),
|
||||
),
|
||||
],
|
||||
|
|
@ -69,3 +165,16 @@ class const CountStepper({
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// [value] up to the next multiple of [step], at most [max]: 12 to 15 in
|
||||
/// steps of 5.
|
||||
int stepUp(int value, int step, {required int max}) =>
|
||||
math.min(max, (value ~/ step + 1) * step);
|
||||
|
||||
/// [value] down to the multiple of [step] below it, never below the first
|
||||
/// step: 12 to 10, 15 to 10 in steps of 5. Smaller numbers are typed.
|
||||
int stepDown(int value, int step) =>
|
||||
math.max(step, ((value - 1) ~/ step) * step);
|
||||
|
||||
/// Whether [value] can step down: it is above the first step.
|
||||
bool canStepDown(int value, int step) => value > step;
|
||||
|
|
|
|||
|
|
@ -242,18 +242,29 @@ class _GoalSheetState() extends State<_GoalSheet> {
|
|||
style: theme.textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// In steps of 5, as the study options count (decided 2026-10-11);
|
||||
// any number in between is typed.
|
||||
CountStepper(
|
||||
value: '$_target',
|
||||
number: _target,
|
||||
style: theme.textTheme.titleMedium,
|
||||
onFewer: _target > StudyGoal.minTarget
|
||||
? () => _setTarget(_target - 1)
|
||||
onFewer: canStepDown(_target, _step)
|
||||
? () => _setTarget(stepDown(_target, _step))
|
||||
: null,
|
||||
onMore: _target < StudyGoal.maxTarget
|
||||
? () => _setTarget(_target + 1)
|
||||
? () => _setTarget(
|
||||
stepUp(_target, _step, max: StudyGoal.maxTarget),
|
||||
)
|
||||
: null,
|
||||
onTyped: (typed) => _setTarget(
|
||||
typed.clamp(StudyGoal.minTarget, StudyGoal.maxTarget),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// How far the goal moves at a step.
|
||||
const _step = 5;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,95 @@
|
|||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/count_stepper.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// A daily limit, such as of new cards: a switch saying whether there is one
|
||||
/// ([title]), and while it is on, its number in steps of [step] up to [max],
|
||||
/// which can also be typed (decided 2026-10-11: that 0 meant no limit had to
|
||||
/// be guessed, and a goal has no such thing). Switched off and on again, it
|
||||
/// comes back as it was; the first time, at [initial].
|
||||
class const DailyLimit({
|
||||
/// What switches the limit on: "Limit new cards per day".
|
||||
required final String title,
|
||||
|
||||
/// The limit; [unlimited] for none.
|
||||
required final int value,
|
||||
|
||||
/// The value standing for no limit.
|
||||
required final int unlimited,
|
||||
|
||||
/// How far a step moves it.
|
||||
required final int step,
|
||||
|
||||
/// The highest it goes.
|
||||
required final int max,
|
||||
|
||||
/// What it starts at, switched on for the first time.
|
||||
required final int initial,
|
||||
|
||||
/// The limit changed.
|
||||
required final ValueChanged<int> onChanged,
|
||||
|
||||
/// What the limit does, under the switch.
|
||||
final String? hint,
|
||||
super.key,
|
||||
}) extends StatefulWidget {
|
||||
@override
|
||||
State<DailyLimit> createState() => _DailyLimitState();
|
||||
}
|
||||
|
||||
class _DailyLimitState() extends State<DailyLimit> {
|
||||
/// The limit before it was switched off.
|
||||
late var _last = widget.value == widget.unlimited
|
||||
? widget.initial
|
||||
: widget.value;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(DailyLimit oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (widget.value != widget.unlimited) {
|
||||
_last = widget.value;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final DailyLimit(:value, :step, :max, :onChanged) = widget;
|
||||
final limited = value != widget.unlimited;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
SwitchListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: Text(widget.title, style: theme.textTheme.titleSmall),
|
||||
subtitle: limited ? null : Text(l10n.studySettingsUnlimited),
|
||||
value: limited,
|
||||
onChanged: (on) => onChanged(on ? _last : widget.unlimited),
|
||||
),
|
||||
if (widget.hint case final hint?)
|
||||
Text(
|
||||
hint,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (limited) ...[
|
||||
const SizedBox(height: 4),
|
||||
CountStepper(
|
||||
value: '$value',
|
||||
number: value,
|
||||
suffix: l10n.studySettingsPerDay,
|
||||
onFewer: canStepDown(value, step)
|
||||
? () => onChanged(stepDown(value, step))
|
||||
: null,
|
||||
onMore: value < max
|
||||
? () => onChanged(stepUp(value, step, max: max))
|
||||
: null,
|
||||
onTyped: (typed) => onChanged(typed.clamp(1, max)),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import 'package:halkyon_learn_flutter/src/core/widget/app_loading_indicator.dart
|
|||
import 'package:halkyon_learn_flutter/src/core/widget/count_stepper.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/bloc/study_settings_bloc.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/model/study_options.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/widget/daily_limit.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/tag/model/tag.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/tag/widget/tag_chip.dart';
|
||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||
|
|
@ -184,32 +185,15 @@ class const _Options({required final StudyOptions options})
|
|||
),
|
||||
if (options.mode == LearnMode.spaced) ...[
|
||||
const SizedBox(height: 20),
|
||||
Text(l10n.studySettingsNewPerDay, style: heading),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
l10n.studySettingsNewPerDayHint,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
CountStepper(
|
||||
value: options.newPerDay == StudyOptions.unlimitedNewPerDay
|
||||
? l10n.studySettingsUnlimited
|
||||
: '${options.newPerDay}',
|
||||
suffix: options.newPerDay == StudyOptions.unlimitedNewPerDay
|
||||
? ''
|
||||
: l10n.studySettingsPerDay,
|
||||
onFewer: options.newPerDay > StudyOptions.unlimitedNewPerDay
|
||||
? () => change(
|
||||
options.copyWith(newPerDay: options.newPerDay - _newStep),
|
||||
)
|
||||
: null,
|
||||
onMore: options.newPerDay < StudyOptions.maxNewPerDay
|
||||
? () => change(
|
||||
options.copyWith(newPerDay: options.newPerDay + _newStep),
|
||||
)
|
||||
: null,
|
||||
DailyLimit(
|
||||
title: l10n.studyLimitNewPerDay,
|
||||
hint: l10n.studySettingsNewPerDayHint,
|
||||
value: options.newPerDay,
|
||||
unlimited: StudyOptions.unlimitedNewPerDay,
|
||||
step: _newStep,
|
||||
max: StudyOptions.maxNewPerDay,
|
||||
initial: const StudyOptions().newPerDay,
|
||||
onChanged: (value) => change(options.copyWith(newPerDay: value)),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/adaptive_sheet.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/count_stepper.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/model/studyset_study_options.dart';
|
||||
import 'package:halkyon_learn_flutter/src/feature/study/widget/daily_limit.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
/// What the study options sheet ended with: the options to keep, or null to
|
||||
|
|
@ -30,6 +30,10 @@ const _newStep = 5;
|
|||
/// How far the daily limit of reviews moves at a step.
|
||||
const _reviewsStep = 25;
|
||||
|
||||
/// The daily limit of reviews the first time it is switched on, as Anki
|
||||
/// starts it.
|
||||
const _initialReviews = 200;
|
||||
|
||||
class const _StudyOptionsSheet({
|
||||
required final StudysetStudyOptions? current,
|
||||
required final StudysetStudyOptions defaults,
|
||||
|
|
@ -49,11 +53,6 @@ class _StudyOptionsSheetState() extends State<_StudyOptionsSheet> {
|
|||
final l10n = AppLocalizations.of(context);
|
||||
final theme = Theme.of(context);
|
||||
final heading = theme.textTheme.titleSmall;
|
||||
String limit(int value) => value == StudysetStudyOptions.unlimited
|
||||
? l10n.studySettingsUnlimited
|
||||
: '$value';
|
||||
String perDay(int value) =>
|
||||
value == StudysetStudyOptions.unlimited ? '' : l10n.studySettingsPerDay;
|
||||
final StudysetStudyOptions(:newPerDay, :reviewsPerDay, :order) = _options;
|
||||
return SheetScaffold(
|
||||
title: l10n.studyOptionsTitle,
|
||||
|
|
@ -94,42 +93,25 @@ class _StudyOptionsSheetState() extends State<_StudyOptionsSheet> {
|
|||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(l10n.studySettingsNewPerDay, style: heading),
|
||||
const SizedBox(height: 8),
|
||||
CountStepper(
|
||||
value: limit(newPerDay),
|
||||
suffix: perDay(newPerDay),
|
||||
onFewer: newPerDay > StudysetStudyOptions.unlimited
|
||||
? () => _change(
|
||||
_options.copyWith(newPerDay: newPerDay - _newStep),
|
||||
)
|
||||
: null,
|
||||
onMore: newPerDay < StudysetStudyOptions.maxNewPerDay
|
||||
? () => _change(
|
||||
_options.copyWith(newPerDay: newPerDay + _newStep),
|
||||
)
|
||||
: null,
|
||||
DailyLimit(
|
||||
title: l10n.studyLimitNewPerDay,
|
||||
value: newPerDay,
|
||||
unlimited: StudysetStudyOptions.unlimited,
|
||||
step: _newStep,
|
||||
max: StudysetStudyOptions.maxNewPerDay,
|
||||
initial: widget.defaults.newPerDay,
|
||||
onChanged: (value) => _change(_options.copyWith(newPerDay: value)),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(l10n.studyOptionsReviewsPerDay, style: heading),
|
||||
const SizedBox(height: 8),
|
||||
CountStepper(
|
||||
value: limit(reviewsPerDay),
|
||||
suffix: perDay(reviewsPerDay),
|
||||
onFewer: reviewsPerDay > StudysetStudyOptions.unlimited
|
||||
? () => _change(
|
||||
_options.copyWith(
|
||||
reviewsPerDay: reviewsPerDay - _reviewsStep,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
onMore: reviewsPerDay < StudysetStudyOptions.maxReviewsPerDay
|
||||
? () => _change(
|
||||
_options.copyWith(
|
||||
reviewsPerDay: reviewsPerDay + _reviewsStep,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
const SizedBox(height: 12),
|
||||
DailyLimit(
|
||||
title: l10n.studyLimitReviewsPerDay,
|
||||
value: reviewsPerDay,
|
||||
unlimited: StudysetStudyOptions.unlimited,
|
||||
step: _reviewsStep,
|
||||
max: StudysetStudyOptions.maxReviewsPerDay,
|
||||
initial: _initialReviews,
|
||||
onChanged: (value) =>
|
||||
_change(_options.copyWith(reviewsPerDay: value)),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(l10n.studyOptionsOrder, style: heading),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:halkyon_learn_flutter/src/core/widget/count_stepper.dart';
|
||||
|
||||
void main() {
|
||||
test('steps go to the next multiple of the step, never below the first', () {
|
||||
expect(stepUp(12, 5, max: 100), 15);
|
||||
expect(stepUp(15, 5, max: 100), 20);
|
||||
expect(stepUp(98, 5, max: 100), 100);
|
||||
expect(stepDown(12, 5), 10);
|
||||
expect(stepDown(15, 5), 10);
|
||||
expect(stepDown(7, 5), 5);
|
||||
expect(canStepDown(5, 5), isFalse);
|
||||
expect(canStepDown(6, 5), isTrue);
|
||||
expect(stepUp(1, 25, max: 1000), 25);
|
||||
expect(stepDown(230, 25), 225);
|
||||
});
|
||||
}
|
||||
|
|
@ -175,7 +175,8 @@ void main() {
|
|||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Weekly goal'), findsOneWidget);
|
||||
expect(find.text('0 of 21 cards this week'), findsOneWidget);
|
||||
// In steps of 5, as the study options.
|
||||
expect(find.text('0 of 25 cards this week'), findsOneWidget);
|
||||
expect(find.byIcon(LucideIcons.pencil), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Weekly goal'));
|
||||
|
|
|
|||
|
|
@ -469,6 +469,8 @@ void main() {
|
|||
|
||||
await tester.tap(find.byTooltip('Decrease').first);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Limit reviews per day'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.byTooltip('Increase').last);
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Mixed'));
|
||||
|
|
@ -480,12 +482,46 @@ void main() {
|
|||
written('japanese'),
|
||||
const StudysetStudyOptions(
|
||||
newPerDay: 15,
|
||||
reviewsPerDay: 25,
|
||||
reviewsPerDay: 225,
|
||||
order: NewCardOrder.mixed,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('a limit steps to the next multiple of its step, takes any '
|
||||
'number typed, and is switched off for none, coming back as it '
|
||||
'was', (tester) async {
|
||||
stored('japanese', 'Japanese');
|
||||
await showLibrary(tester);
|
||||
await choose(tester, 'Japanese', 'Study options');
|
||||
Finder newCards() => find.text('20 / day', findRichText: true);
|
||||
expect(newCards(), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byTooltip('Type a number').first);
|
||||
await tester.pump();
|
||||
await tester.enterText(find.byType(TextField), '12');
|
||||
await tester.testTextInput.receiveAction(TextInputAction.done);
|
||||
await tester.pump();
|
||||
expect(find.text('12 / day', findRichText: true), findsOneWidget);
|
||||
await tester.tap(find.byTooltip('Increase').first);
|
||||
await tester.pump();
|
||||
expect(find.text('15 / day', findRichText: true), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Limit new cards per day'));
|
||||
await tester.pump();
|
||||
expect(find.text('15 / day', findRichText: true), findsNothing);
|
||||
expect(find.text('Unlimited'), findsNWidgets(2));
|
||||
await tester.tap(find.text('Limit new cards per day'));
|
||||
await tester.pump();
|
||||
expect(find.text('15 / day', findRichText: true), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text('Limit new cards per day'));
|
||||
await tester.pump();
|
||||
await tester.tap(find.text('Save'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(written('japanese')?.newPerDay, StudysetStudyOptions.unlimited);
|
||||
});
|
||||
|
||||
testWidgets('saved as they came, the defaults stay the defaults', (
|
||||
tester,
|
||||
) async {
|
||||
|
|
|
|||
Loading…
Reference in a new issue