Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions mobile/apps/subturtle/lib/features/me/me_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ class _LanguagePickerState extends State<_LanguagePicker> {
class _LanguageGroups extends ConsumerWidget {
const _LanguageGroups();

static const _perDay = [3, 5, 8, 10, 15];
static const _perDay = [3, 5, 8, 10, 15, 20, 30];

@override
Widget build(BuildContext context, WidgetRef ref) {
Expand Down Expand Up @@ -567,7 +567,8 @@ class _LanguageGroups extends ConsumerWidget {
children: [
Text('How many new $name phrases come from the Pool each day. This setting is for $name only.', style: StType.bodyMedium.copyWith(color: ctx.st.textMuted)),
const SizedBox(height: 12),
for (final n in _perDay) ...[
// The current value is always a choice, even one set on the web.
for (final n in {..._perDay, ?current}.toList()..sort()) ...[
StLanguageOption(code: '$n', title: Fmt.plural(n, 'phrase'), selected: n == current, onTap: () => Navigator.pop(ctx, n)),
const SizedBox(height: 8),
],
Expand Down
12 changes: 10 additions & 2 deletions mobile/apps/subturtle/test/fake_languages.dart
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ class FakeLanguageServer {
} else {
final level = (a['startLevel'] as String?) ?? 'A1';
langs[code] = _Lang(code, level, (a['startLevelSource'] as String?) ?? 'default')
..pack = _hasPack(code) ? 'offered' : 'none';
..pack = _hasPack(code) ? 'offered' : 'none'
..goals = [...?(a['goals'] as List?)?.cast<String>()];
}
pending = {...pending}..remove(code);
active = code;
Expand Down Expand Up @@ -175,6 +176,7 @@ class FakeLanguageServer {
final l = langs[a['language']]!;
if (a['explanationLanguage'] != null) l.explanationLanguage = a['explanationLanguage'] as String;
if (a['newPhrasesPerDay'] != null) l.newPerDay = a['newPhrasesPerDay'] as int;
if (a['goals'] != null) l.goals = (a['goals'] as List).cast<String>();
return _wrapped();
});
on('set-translation-language', (a) {
Expand Down Expand Up @@ -284,7 +286,8 @@ class _Lang {
String status = 'active';
String pack = 'none';
String? explanationLanguage;
int newPerDay = 10;
int newPerDay = 20;
List<String> goals = const [];
final heard = <String>{};
int heardToday = 0;

Expand All @@ -297,6 +300,11 @@ class _Lang {
'phraseCount': phrases,
'explanationLanguage': explanationLanguage ?? l1,
'starterPack': {'state': pack, 'heard': heard.length, 'total': pack == 'none' ? 0 : 4},
// Per-language settings, as get-language-state returns them (#134).
'newPhrasesPerDay': newPerDay,
'poolChunkSize': 10,
'poolAgeCutoffDays': 14,
'goals': goals,
if (status == 'removed') 'purgeAt': '2026-11-15T00:00:00.000Z',
};
}
10 changes: 10 additions & 0 deletions mobile/apps/subturtle/test/languages_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,16 @@ void main() {
expect(upd['explanationLanguage'], 'tr');

await tester.scrollUntilVisible(find.text('New phrases a day'), 100);
// The current value from get-language-state (server default 20).
expect(find.descendant(of: find.ancestor(of: find.text('New phrases a day'), matching: find.byType(Row)).first, matching: find.text('20')), findsOneWidget);
await tester.tap(find.text('New phrases a day'));
await tester.settle();
expect(find.bySemanticsLabel(RegExp('^20 phrases.*active')), findsOneWidget, reason: 'the current value is ticked');
await tester.tap(find.text('5 phrases'));
await tester.settle();
expect(langs.langs['en']!.newPerDay, 5);
expect(find.descendant(of: find.ancestor(of: find.text('New phrases a day'), matching: find.byType(Row)).first, matching: find.text('5')), findsOneWidget);
// Again, to keep the original assertions below working.
await tester.tap(find.text('New phrases a day'));
await tester.settle();
await tester.tap(find.text('5 phrases'));
Expand Down
16 changes: 15 additions & 1 deletion mobile/packages/subturtle_api/lib/src/models/language.dart
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@ class LanguageSummary {
this.starterPack = const StarterPackSummary(),
this.purgeAt,
this.newPhrasesPerDay,
this.poolChunkSize,
this.poolAgeCutoffDays,
this.goals = const [],
});

final String code;
Expand All @@ -293,9 +296,17 @@ class LanguageSummary {
/// When a removed language is deleted for good.
final DateTime? purgeAt;

/// New phrases a day from the Pool, when the server sends it.
/// New phrases a day from the Pool (server default 20). Null on servers
/// that do not send it.
final int? newPhrasesPerDay;

/// Pool settings of this language.
final int? poolChunkSize;
final int? poolAgeCutoffDays;

/// Why the learner studies it (`watch`, `work`, `travel`, `curious`).
final List<String> goals;

bool get isActive => status == 'active';
bool get isPaused => status == 'paused';
bool get isRemoved => status == 'removed';
Expand All @@ -314,6 +325,9 @@ class LanguageSummary {
starterPack: StarterPackSummary.fromJson(j['starterPack']),
purgeAt: date(j['purgeAt']),
newPhrasesPerDay: intOrNull(j['newPhrasesPerDay'] ?? asJson(j['settings'])['newPhrasesPerDay']),
poolChunkSize: intOrNull(j['poolChunkSize'] ?? asJson(j['settings'])['poolChunkSize']),
poolAgeCutoffDays: intOrNull(j['poolAgeCutoffDays'] ?? asJson(j['settings'])['poolAgeCutoffDays']),
goals: listOf(j['goals'], (e) => str(e)),
);
}
}
Expand Down
5 changes: 4 additions & 1 deletion mobile/packages/subturtle_api/test/language_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Map<String, dynamic> stateJson({String? active = 'de', String plan = 'reader', i
'learningLanguage': active,
'languages': [
{'code': 'en', 'status': 'active', 'startLevel': 'B1', 'startLevelSource': 'test', 'dueCount': 12, 'phraseCount': 142, 'explanationLanguage': 'fa', 'starterPack': {'state': 'none', 'heard': 0, 'total': 0}},
{'code': 'de', 'status': 'active', 'startLevel': 'A1', 'startLevelSource': 'default', 'dueCount': 7, 'phraseCount': 38, 'explanationLanguage': 'fa', 'starterPack': {'state': 'in_progress', 'heard': 12, 'total': 150}},
{'code': 'de', 'status': 'active', 'startLevel': 'A1', 'startLevelSource': 'default', 'dueCount': 7, 'phraseCount': 38, 'explanationLanguage': 'fa', 'starterPack': {'state': 'in_progress', 'heard': 12, 'total': 150}, 'newPhrasesPerDay': 20, 'poolChunkSize': 10, 'poolAgeCutoffDays': 14, 'goals': ['watch']},
{'code': 'it', 'status': 'paused', 'startLevel': 'A1', 'startLevelSource': 'default', 'dueCount': 0, 'phraseCount': 3, 'explanationLanguage': 'fa', 'starterPack': {'state': 'none', 'heard': 0, 'total': 0}},
{'code': 'fr', 'status': 'removed', 'startLevel': 'A1', 'startLevelSource': 'default', 'dueCount': 0, 'phraseCount': 5, 'explanationLanguage': 'fa', 'purgeAt': '2026-11-15T00:00:00.000Z', 'starterPack': {'state': 'none', 'heard': 0, 'total': 0}},
{'code': 'sv', 'status': 'purging', 'startLevel': 'A1', 'startLevelSource': 'default', 'dueCount': 0, 'phraseCount': 0},
Expand All @@ -36,6 +36,9 @@ void main() {
expect(s.paused.single.code, 'it');
expect(s.removed.single.purgeAt, isNotNull);
expect(s.summaryOf('de')!.starterPack.heard, 12);
final de = s.summaryOf('de')!;
expect([de.newPhrasesPerDay, de.poolChunkSize, de.poolAgeCutoffDays], [20, 10, 14]);
expect(de.goals, ['watch']);
expect(s.summaryOf('en')!.fromTest, isTrue);
expect(s.used, 2);
expect(s.atLimit, isFalse);
Expand Down
5 changes: 5 additions & 0 deletions mobile/packages/subturtle_api/test/live_server_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,11 @@ void main() {

s = await api.languages.updateSettings('de', explanationLanguage: 'fa');
expect(s.summaryOf('de')!.explanationLanguage, 'fa');
// Per-language settings come back with the state (server #134).
expect(s.summaryOf('de')!.newPhrasesPerDay, isNotNull);
s = await api.languages.updateSettings('de', newPhrasesPerDay: 8);
expect(s.summaryOf('de')!.newPhrasesPerDay, 8);
expect(s.summaryOf('de')!.goals, ['watch']);

expect(await api.languages.setInterest('pt', on: true), isTrue);

Expand Down
Loading