feat(learn): pick up learning on Home, the studysets studied latest first, and Progress closer to Vaia's
Home gets a widget of its own, Pick up learning, below the goal: the studysets at the top of the library that were studied, the one rated last first, so one studied long ago sinks. Three show, the others behind Show more. Each shows its name, which opens it, how much is learnt, when it was last studied, and today's task (cards to review, new cards to learn), which starts studying it, outlined while reviews wait. The progress is the library's, worked out once for every screen; when each was last studied comes from the reviews Home reads anyway. With nothing studied it takes no room. A dashboard arranged before it gets the widget below the goal, wherever the goal is: a widget a layout misses now goes after the one before it in a new dashboard, no longer at the end. On a studyset's Progress, the tiles are filled without an outline, To review has a history icon (amber while cards are due), the learnt bar is thicker, and the exam follows the figures, as Vaia asks for its date there: an invitation to choose it, opening the study plan's sheet, or, once chosen, how ready the person is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
447e00c52a
commit
a0bb75f1ad
17 changed files with 976 additions and 40 deletions
|
|
@ -1380,6 +1380,16 @@
|
||||||
"readerCaptureFailed": "Die Seite konnte nicht dargestellt werden. Versuche es erneut, sobald sie geladen ist.",
|
"readerCaptureFailed": "Die Seite konnte nicht dargestellt werden. Versuche es erneut, sobald sie geladen ist.",
|
||||||
"readerScreenshotSaved": "Bildschirmfoto gespeichert",
|
"readerScreenshotSaved": "Bildschirmfoto gespeichert",
|
||||||
"imageOpenPdfPage": "PDF auf dieser Seite öffnen",
|
"imageOpenPdfPage": "PDF auf dieser Seite öffnen",
|
||||||
|
"homeWidgetPickUp": "Weiterlernen",
|
||||||
|
"homePickUpPlaceholder": "Die Lernsets, die du lernst, das zuletzt gelernte zuerst",
|
||||||
|
"homePickUpStudied": "{days, plural, =0{Heute gelernt} =1{Gestern gelernt} other{Vor {days} Tagen gelernt}}",
|
||||||
|
"homePickUpLearned": "{percent} % gelernt",
|
||||||
|
"homePickUpReview": "{count, plural, =1{1 Karte wiederholen} other{{count} Karten wiederholen}}",
|
||||||
|
"homePickUpLearn": "{count, plural, =1{1 neue Karte lernen} other{{count} neue Karten lernen}}",
|
||||||
|
"homePickUpReviewAndLearn": "{reviews} wiederholen, {fresh} neue lernen",
|
||||||
|
"homePickUpDone": "Heute nichts fällig",
|
||||||
|
"homePickUpMore": "{count} weitere zeigen",
|
||||||
|
"homePickUpFewer": "Weniger zeigen",
|
||||||
"homeWidgetDeadlines": "PDF-Lesefristen",
|
"homeWidgetDeadlines": "PDF-Lesefristen",
|
||||||
"deadlinesTitle": "Lesefristen",
|
"deadlinesTitle": "Lesefristen",
|
||||||
"deadlinesCurrent": "Aktuell",
|
"deadlinesCurrent": "Aktuell",
|
||||||
|
|
@ -1953,6 +1963,9 @@
|
||||||
"progressToReview": "Zu wiederholen",
|
"progressToReview": "Zu wiederholen",
|
||||||
"progressToReviewInfo": "Schon gelernte Karten, die heute wieder fällig sind.",
|
"progressToReviewInfo": "Schon gelernte Karten, die heute wieder fällig sind.",
|
||||||
"progressDueToday": "{count, plural, =1{Karte heute fällig} other{Karten heute fällig}}",
|
"progressDueToday": "{count, plural, =1{Karte heute fällig} other{Karten heute fällig}}",
|
||||||
|
"progressExamTitle": "Bereitest du dich auf eine Prüfung vor?",
|
||||||
|
"progressExamHint": "Wähle sie und ihr Datum, um zu sehen, wie bereit du bist.",
|
||||||
|
"progressExamChoose": "Prüfung wählen",
|
||||||
"progressStudyTime": "Lernzeit",
|
"progressStudyTime": "Lernzeit",
|
||||||
"progressStudyTimeInfo": "Wie lange du die Karteikarten dieses Lernsets in den letzten sieben Tagen gelernt hast, jede Karte höchstens eine Minute gezählt.",
|
"progressStudyTimeInfo": "Wie lange du die Karteikarten dieses Lernsets in den letzten sieben Tagen gelernt hast, jede Karte höchstens eine Minute gezählt.",
|
||||||
"progressThisWeek": "diese Woche",
|
"progressThisWeek": "diese Woche",
|
||||||
|
|
|
||||||
|
|
@ -1577,6 +1577,22 @@
|
||||||
"readerCaptureFailed": "The page couldn't be drawn. Try again once it has loaded.",
|
"readerCaptureFailed": "The page couldn't be drawn. Try again once it has loaded.",
|
||||||
"readerScreenshotSaved": "Screenshot saved",
|
"readerScreenshotSaved": "Screenshot saved",
|
||||||
"imageOpenPdfPage": "Open the PDF at this page",
|
"imageOpenPdfPage": "Open the PDF at this page",
|
||||||
|
"homeWidgetPickUp": "Pick up learning",
|
||||||
|
"homePickUpPlaceholder": "The studysets you study, the latest first",
|
||||||
|
"homePickUpStudied": "{days, plural, =0{Studied today} =1{Studied yesterday} other{Studied {days} days ago}}",
|
||||||
|
"@homePickUpStudied": {"placeholders": {"days": {"type": "int"}}},
|
||||||
|
"homePickUpLearned": "{percent} % learned",
|
||||||
|
"@homePickUpLearned": {"placeholders": {"percent": {"type": "int"}}},
|
||||||
|
"homePickUpReview": "{count, plural, =1{Review 1 card} other{Review {count} cards}}",
|
||||||
|
"@homePickUpReview": {"placeholders": {"count": {"type": "int"}}},
|
||||||
|
"homePickUpLearn": "{count, plural, =1{Learn 1 new card} other{Learn {count} new cards}}",
|
||||||
|
"@homePickUpLearn": {"placeholders": {"count": {"type": "int"}}},
|
||||||
|
"homePickUpReviewAndLearn": "Review {reviews}, learn {fresh} new",
|
||||||
|
"@homePickUpReviewAndLearn": {"placeholders": {"reviews": {"type": "int"}, "fresh": {"type": "int"}}},
|
||||||
|
"homePickUpDone": "Nothing due today",
|
||||||
|
"homePickUpMore": "Show {count} more",
|
||||||
|
"@homePickUpMore": {"placeholders": {"count": {"type": "int"}}},
|
||||||
|
"homePickUpFewer": "Show fewer",
|
||||||
"homeWidgetDeadlines": "PDF reading deadlines",
|
"homeWidgetDeadlines": "PDF reading deadlines",
|
||||||
"deadlinesTitle": "Reading deadlines",
|
"deadlinesTitle": "Reading deadlines",
|
||||||
"deadlinesCurrent": "Current",
|
"deadlinesCurrent": "Current",
|
||||||
|
|
@ -2329,6 +2345,9 @@
|
||||||
"progressToReviewInfo": "Cards studied before that are due again today.",
|
"progressToReviewInfo": "Cards studied before that are due again today.",
|
||||||
"progressDueToday": "{count, plural, =1{card due today} other{cards due today}}",
|
"progressDueToday": "{count, plural, =1{card due today} other{cards due today}}",
|
||||||
"@progressDueToday": {"placeholders": {"count": {"type": "int"}}},
|
"@progressDueToday": {"placeholders": {"count": {"type": "int"}}},
|
||||||
|
"progressExamTitle": "Preparing for an exam?",
|
||||||
|
"progressExamHint": "Choose it and its date to see how ready you are.",
|
||||||
|
"progressExamChoose": "Choose the exam",
|
||||||
"progressStudyTime": "Study time",
|
"progressStudyTime": "Study time",
|
||||||
"progressStudyTimeInfo": "How long you studied this studyset's flashcards in the last seven days, each card counted for at most a minute.",
|
"progressStudyTimeInfo": "How long you studied this studyset's flashcards in the last seven days, each card counted for at most a minute.",
|
||||||
"progressThisWeek": "this week",
|
"progressThisWeek": "this week",
|
||||||
|
|
|
||||||
|
|
@ -8140,6 +8140,66 @@ abstract class AppLocalizations {
|
||||||
/// **'Open the PDF at this page'**
|
/// **'Open the PDF at this page'**
|
||||||
String get imageOpenPdfPage;
|
String get imageOpenPdfPage;
|
||||||
|
|
||||||
|
/// No description provided for @homeWidgetPickUp.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Pick up learning'**
|
||||||
|
String get homeWidgetPickUp;
|
||||||
|
|
||||||
|
/// No description provided for @homePickUpPlaceholder.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'The studysets you study, the latest first'**
|
||||||
|
String get homePickUpPlaceholder;
|
||||||
|
|
||||||
|
/// No description provided for @homePickUpStudied.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{days, plural, =0{Studied today} =1{Studied yesterday} other{Studied {days} days ago}}'**
|
||||||
|
String homePickUpStudied(int days);
|
||||||
|
|
||||||
|
/// No description provided for @homePickUpLearned.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{percent} % learned'**
|
||||||
|
String homePickUpLearned(int percent);
|
||||||
|
|
||||||
|
/// No description provided for @homePickUpReview.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{count, plural, =1{Review 1 card} other{Review {count} cards}}'**
|
||||||
|
String homePickUpReview(int count);
|
||||||
|
|
||||||
|
/// No description provided for @homePickUpLearn.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'{count, plural, =1{Learn 1 new card} other{Learn {count} new cards}}'**
|
||||||
|
String homePickUpLearn(int count);
|
||||||
|
|
||||||
|
/// No description provided for @homePickUpReviewAndLearn.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Review {reviews}, learn {fresh} new'**
|
||||||
|
String homePickUpReviewAndLearn(int reviews, int fresh);
|
||||||
|
|
||||||
|
/// No description provided for @homePickUpDone.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Nothing due today'**
|
||||||
|
String get homePickUpDone;
|
||||||
|
|
||||||
|
/// No description provided for @homePickUpMore.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Show {count} more'**
|
||||||
|
String homePickUpMore(int count);
|
||||||
|
|
||||||
|
/// No description provided for @homePickUpFewer.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Show fewer'**
|
||||||
|
String get homePickUpFewer;
|
||||||
|
|
||||||
/// No description provided for @homeWidgetDeadlines.
|
/// No description provided for @homeWidgetDeadlines.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
@ -11534,6 +11594,24 @@ abstract class AppLocalizations {
|
||||||
/// **'{count, plural, =1{card due today} other{cards due today}}'**
|
/// **'{count, plural, =1{card due today} other{cards due today}}'**
|
||||||
String progressDueToday(int count);
|
String progressDueToday(int count);
|
||||||
|
|
||||||
|
/// No description provided for @progressExamTitle.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Preparing for an exam?'**
|
||||||
|
String get progressExamTitle;
|
||||||
|
|
||||||
|
/// No description provided for @progressExamHint.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Choose it and its date to see how ready you are.'**
|
||||||
|
String get progressExamHint;
|
||||||
|
|
||||||
|
/// No description provided for @progressExamChoose.
|
||||||
|
///
|
||||||
|
/// In en, this message translates to:
|
||||||
|
/// **'Choose the exam'**
|
||||||
|
String get progressExamChoose;
|
||||||
|
|
||||||
/// No description provided for @progressStudyTime.
|
/// No description provided for @progressStudyTime.
|
||||||
///
|
///
|
||||||
/// In en, this message translates to:
|
/// In en, this message translates to:
|
||||||
|
|
|
||||||
|
|
@ -5006,6 +5006,68 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get imageOpenPdfPage => 'PDF auf dieser Seite öffnen';
|
String get imageOpenPdfPage => 'PDF auf dieser Seite öffnen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeWidgetPickUp => 'Weiterlernen';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homePickUpPlaceholder =>
|
||||||
|
'Die Lernsets, die du lernst, das zuletzt gelernte zuerst';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String homePickUpStudied(int days) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
days,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'Vor $days Tagen gelernt',
|
||||||
|
one: 'Gestern gelernt',
|
||||||
|
zero: 'Heute gelernt',
|
||||||
|
);
|
||||||
|
return '$_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String homePickUpLearned(int percent) {
|
||||||
|
return '$percent % gelernt';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String homePickUpReview(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: '$count Karten wiederholen',
|
||||||
|
one: '1 Karte wiederholen',
|
||||||
|
);
|
||||||
|
return '$_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String homePickUpLearn(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: '$count neue Karten lernen',
|
||||||
|
one: '1 neue Karte lernen',
|
||||||
|
);
|
||||||
|
return '$_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String homePickUpReviewAndLearn(int reviews, int fresh) {
|
||||||
|
return '$reviews wiederholen, $fresh neue lernen';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homePickUpDone => 'Heute nichts fällig';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String homePickUpMore(int count) {
|
||||||
|
return '$count weitere zeigen';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homePickUpFewer => 'Weniger zeigen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get homeWidgetDeadlines => 'PDF-Lesefristen';
|
String get homeWidgetDeadlines => 'PDF-Lesefristen';
|
||||||
|
|
||||||
|
|
@ -7301,6 +7363,16 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||||
return '$_temp0';
|
return '$_temp0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get progressExamTitle => 'Bereitest du dich auf eine Prüfung vor?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get progressExamHint =>
|
||||||
|
'Wähle sie und ihr Datum, um zu sehen, wie bereit du bist.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get progressExamChoose => 'Prüfung wählen';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get progressStudyTime => 'Lernzeit';
|
String get progressStudyTime => 'Lernzeit';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4959,6 +4959,68 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
@override
|
@override
|
||||||
String get imageOpenPdfPage => 'Open the PDF at this page';
|
String get imageOpenPdfPage => 'Open the PDF at this page';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homeWidgetPickUp => 'Pick up learning';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homePickUpPlaceholder =>
|
||||||
|
'The studysets you study, the latest first';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String homePickUpStudied(int days) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
days,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'Studied $days days ago',
|
||||||
|
one: 'Studied yesterday',
|
||||||
|
zero: 'Studied today',
|
||||||
|
);
|
||||||
|
return '$_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String homePickUpLearned(int percent) {
|
||||||
|
return '$percent % learned';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String homePickUpReview(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'Review $count cards',
|
||||||
|
one: 'Review 1 card',
|
||||||
|
);
|
||||||
|
return '$_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String homePickUpLearn(int count) {
|
||||||
|
String _temp0 = intl.Intl.pluralLogic(
|
||||||
|
count,
|
||||||
|
locale: localeName,
|
||||||
|
other: 'Learn $count new cards',
|
||||||
|
one: 'Learn 1 new card',
|
||||||
|
);
|
||||||
|
return '$_temp0';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String homePickUpReviewAndLearn(int reviews, int fresh) {
|
||||||
|
return 'Review $reviews, learn $fresh new';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homePickUpDone => 'Nothing due today';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String homePickUpMore(int count) {
|
||||||
|
return 'Show $count more';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get homePickUpFewer => 'Show fewer';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get homeWidgetDeadlines => 'PDF reading deadlines';
|
String get homeWidgetDeadlines => 'PDF reading deadlines';
|
||||||
|
|
||||||
|
|
@ -7233,6 +7295,16 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||||
return '$_temp0';
|
return '$_temp0';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get progressExamTitle => 'Preparing for an exam?';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get progressExamHint =>
|
||||||
|
'Choose it and its date to see how ready you are.';
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get progressExamChoose => 'Choose the exam';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String get progressStudyTime => 'Study time';
|
String get progressStudyTime => 'Study time';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import 'package:equatable/equatable.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/card/data/card_repository.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/home/model/home_layout.dart';
|
import 'package:halkyon_learn_flutter/src/feature/home/model/home_layout.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/home/model/pick_up.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/home/model/study_activity.dart';
|
import 'package:halkyon_learn_flutter/src/feature/home/model/study_activity.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/home/model/study_goal.dart';
|
import 'package:halkyon_learn_flutter/src/feature/home/model/study_goal.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_progress_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/learning_path/data/path_progress_repository.dart';
|
||||||
|
|
@ -17,8 +18,9 @@ import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_tree.dart';
|
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_tree.dart';
|
||||||
|
|
||||||
/// The home dashboard: the streak, the study goal and how far it got, the
|
/// The home dashboard: the streak, the study goal and how far it got, the
|
||||||
/// week with its rest days and the cards falling due, the activity of the
|
/// studysets to pick up learning in, the week with its rest days and the
|
||||||
/// past year, and how the person arranged it. It follows every rating, card,
|
/// cards falling due, the activity of the past year, and how the person
|
||||||
|
/// arranged it. It follows every rating, card,
|
||||||
/// studyset and setting, here or synced.
|
/// studyset and setting, here or synced.
|
||||||
class HomeBloc(
|
class HomeBloc(
|
||||||
final StudysetRepository _studysets,
|
final StudysetRepository _studysets,
|
||||||
|
|
@ -144,6 +146,9 @@ class HomeBloc(
|
||||||
if (!resting.contains(card.studysetId)) card.id,
|
if (!resting.contains(card.studysetId)) card.id,
|
||||||
];
|
];
|
||||||
final schedules = _reviews.schedulesFrom(reviews, active);
|
final schedules = _reviews.schedulesFrom(reviews, active);
|
||||||
|
final pickUp = pickUpOf(notDeleted(everything), {
|
||||||
|
for (final card in cards) card.id: card.studysetId,
|
||||||
|
}, reviews);
|
||||||
if (refresh != _refreshes) {
|
if (refresh != _refreshes) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -157,6 +162,7 @@ class HomeBloc(
|
||||||
due: dueByDay([
|
due: dueByDay([
|
||||||
for (final schedule in schedules.values) ?schedule.due,
|
for (final schedule in schedules.values) ?schedule.due,
|
||||||
], localDay(now)),
|
], localDay(now)),
|
||||||
|
pickUp: pickUp,
|
||||||
goal: settings.goal,
|
goal: settings.goal,
|
||||||
goalProgress: switch (settings.goal) {
|
goalProgress: switch (settings.goal) {
|
||||||
final goal? => goalProgress(goal, ratings, now),
|
final goal? => goalProgress(goal, ratings, now),
|
||||||
|
|
@ -245,6 +251,9 @@ final class const HomeStateLoaded({
|
||||||
/// How many cards fall due on each day.
|
/// How many cards fall due on each day.
|
||||||
required final Map<DateTime, int> due,
|
required final Map<DateTime, int> due,
|
||||||
|
|
||||||
|
/// The studysets to pick up learning in, the one studied last first.
|
||||||
|
required final List<PickUp> pickUp,
|
||||||
|
|
||||||
/// The study goal; null without one.
|
/// The study goal; null without one.
|
||||||
required final StudyGoal? goal,
|
required final StudyGoal? goal,
|
||||||
|
|
||||||
|
|
@ -261,6 +270,7 @@ final class const HomeStateLoaded({
|
||||||
restDays,
|
restDays,
|
||||||
streak,
|
streak,
|
||||||
due,
|
due,
|
||||||
|
pickUp,
|
||||||
goal,
|
goal,
|
||||||
goalProgress,
|
goalProgress,
|
||||||
layout,
|
layout,
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,9 @@ enum HomeWidget() {
|
||||||
/// The study goal.
|
/// The study goal.
|
||||||
goal,
|
goal,
|
||||||
|
|
||||||
|
/// The studysets studied, the latest first, to pick up learning in.
|
||||||
|
pickUp,
|
||||||
|
|
||||||
/// The Pomodoro timer.
|
/// The Pomodoro timer.
|
||||||
pomodoro,
|
pomodoro,
|
||||||
|
|
||||||
|
|
@ -29,16 +32,23 @@ final class const HomeLayout({
|
||||||
final Set<HomeWidget> hidden = const {},
|
final Set<HomeWidget> hidden = const {},
|
||||||
}) with Equatable {
|
}) with Equatable {
|
||||||
/// The layout with [order] (widgets a newer version knows dropped,
|
/// The layout with [order] (widgets a newer version knows dropped,
|
||||||
/// widgets it misses added at the end) and [hidden].
|
/// widgets it misses added each after the one before it in a new
|
||||||
factory reconciled(List<HomeWidget> order, Set<HomeWidget> hidden) =>
|
/// dashboard, so that a widget new in this version shows where a new
|
||||||
HomeLayout(
|
/// dashboard has it, not below everything) and [hidden].
|
||||||
order: [
|
factory reconciled(List<HomeWidget> order, Set<HomeWidget> hidden) {
|
||||||
...{...order},
|
final reconciled = [
|
||||||
for (final widget in HomeWidget.values)
|
...{...order},
|
||||||
if (!order.contains(widget)) widget,
|
];
|
||||||
],
|
for (final (index, widget) in HomeWidget.values.indexed) {
|
||||||
hidden: hidden,
|
if (!reconciled.contains(widget)) {
|
||||||
);
|
final before = index == 0
|
||||||
|
? -1
|
||||||
|
: reconciled.indexOf(HomeWidget.values[index - 1]);
|
||||||
|
reconciled.insert(before + 1, widget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return HomeLayout(order: reconciled, hidden: hidden);
|
||||||
|
}
|
||||||
|
|
||||||
/// The widgets shown, top to bottom.
|
/// The widgets shown, top to bottom.
|
||||||
List<HomeWidget> get shown => [
|
List<HomeWidget> get shown => [
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/study/model/review.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_tree.dart';
|
||||||
|
|
||||||
|
/// A studyset to pick up learning in, on Home (decided 2026-11-02, after
|
||||||
|
/// Vaia's): one of the person's own at the top of the library, and when a
|
||||||
|
/// card in it or in its topics was last rated.
|
||||||
|
final class const PickUp({
|
||||||
|
/// The studyset.
|
||||||
|
required final Studyset studyset,
|
||||||
|
|
||||||
|
/// When a card in it was last rated, in UTC.
|
||||||
|
required final DateTime lastStudied,
|
||||||
|
}) with Equatable {
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [studyset, lastStudied];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The studysets at the top of [studysets] (none deleted) that were
|
||||||
|
/// studied, the one studied last first: rated in [reviews], a card counted
|
||||||
|
/// in the studyset [studysetOfCard] gives it and in the studyset at the
|
||||||
|
/// top above that. A fresh start of a card is no studying, and an archived
|
||||||
|
/// studyset rests, as it falls due nowhere.
|
||||||
|
List<PickUp> pickUpOf(
|
||||||
|
List<Studyset> studysets,
|
||||||
|
Map<String, String> studysetOfCard,
|
||||||
|
Iterable<Review> reviews,
|
||||||
|
) {
|
||||||
|
final tops = {
|
||||||
|
for (final studyset in topLevel(studysets))
|
||||||
|
if (!studyset.isArchived) studyset.id: studyset,
|
||||||
|
};
|
||||||
|
final topOf = <String, String>{
|
||||||
|
for (final top in tops.keys)
|
||||||
|
for (final id in subtreeOf(top, studysets)) id: top,
|
||||||
|
};
|
||||||
|
final last = <String, DateTime>{};
|
||||||
|
for (final review in reviews) {
|
||||||
|
if (!review.mode.rates) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final top = topOf[studysetOfCard[review.cardId]];
|
||||||
|
if (top == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final at = review.reviewedAt;
|
||||||
|
if (last[top] case final before? when !at.isAfter(before)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
last[top] = at;
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
for (final MapEntry(key: id, value: at) in last.entries)
|
||||||
|
PickUp(studyset: tops[id]!, lastStudied: at),
|
||||||
|
]..sort((a, b) => b.lastStudied.compareTo(a.lastStudied));
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,7 @@ import 'package:halkyon_learn_flutter/src/feature/home/model/home_layout.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/home/model/study_activity.dart';
|
import 'package:halkyon_learn_flutter/src/feature/home/model/study_activity.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/home/widget/activity_heatmap.dart';
|
import 'package:halkyon_learn_flutter/src/feature/home/widget/activity_heatmap.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/home/widget/goal_card.dart';
|
import 'package:halkyon_learn_flutter/src/feature/home/widget/goal_card.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/home/widget/pick_up_card.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/home/widget/streak.dart';
|
import 'package:halkyon_learn_flutter/src/feature/home/widget/streak.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/home/widget/week_overview.dart';
|
import 'package:halkyon_learn_flutter/src/feature/home/widget/week_overview.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/pdf_source/widget/reading_deadlines_card.dart';
|
import 'package:halkyon_learn_flutter/src/feature/pdf_source/widget/reading_deadlines_card.dart';
|
||||||
|
|
@ -21,7 +22,8 @@ import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
import 'package:material_ui/material_ui.dart';
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
/// The home tab, where the app opens: a study dashboard. The streak sits in
|
/// The home tab, where the app opens: a study dashboard. The streak sits in
|
||||||
/// the app bar; below it the study goal, the week and a year of activity,
|
/// the app bar; below it the study goal, the studysets to pick up learning
|
||||||
|
/// in, the week and a year of activity,
|
||||||
/// in the order the person arranged them. Cobalt washes down from the top
|
/// in the order the person arranged them. Cobalt washes down from the top
|
||||||
/// ([TopGradient]). Where the person searches the web or asks an assistant
|
/// ([TopGradient]). Where the person searches the web or asks an assistant
|
||||||
/// from the app, its button stands at the lower left ([AssistantButton]).
|
/// from the app, its button stands at the lower left ([AssistantButton]).
|
||||||
|
|
@ -199,6 +201,11 @@ class _HomeState() extends State<_Home> {
|
||||||
progress: state.goalProgress,
|
progress: state.goalProgress,
|
||||||
onChanged: (goal) => _send(HomeEventGoalSet(goal)),
|
onChanged: (goal) => _send(HomeEventGoalSet(goal)),
|
||||||
),
|
),
|
||||||
|
HomeWidget.pickUp => PickUpCard(
|
||||||
|
pickUp: state.pickUp,
|
||||||
|
now: state.now,
|
||||||
|
placeholder: _arranging,
|
||||||
|
),
|
||||||
HomeWidget.deadlines => ReadingDeadlinesCard(placeholder: _arranging),
|
HomeWidget.deadlines => ReadingDeadlinesCard(placeholder: _arranging),
|
||||||
HomeWidget.week => Card(
|
HomeWidget.week => Card(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
|
@ -255,9 +262,11 @@ class _HomeState() extends State<_Home> {
|
||||||
// Taps still reach the widget's own controls.
|
// Taps still reach the widget's own controls.
|
||||||
behavior: HitTestBehavior.translucent,
|
behavior: HitTestBehavior.translucent,
|
||||||
onLongPress: _arrange,
|
onLongPress: _arrange,
|
||||||
// The reading deadlines leave no gap while they show
|
// The reading deadlines and Pick up learning leave no
|
||||||
// nothing: they keep their own.
|
// gap while they show nothing: they keep their own.
|
||||||
child: widget == HomeWidget.deadlines
|
child:
|
||||||
|
widget == HomeWidget.deadlines ||
|
||||||
|
widget == HomeWidget.pickUp
|
||||||
? section(widget)
|
? section(widget)
|
||||||
: Padding(
|
: Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
|
|
@ -328,6 +337,7 @@ class const _Arranging({
|
||||||
String label(HomeWidget widget) => switch (widget) {
|
String label(HomeWidget widget) => switch (widget) {
|
||||||
HomeWidget.pomodoro => l10n.pomodoroTitle,
|
HomeWidget.pomodoro => l10n.pomodoroTitle,
|
||||||
HomeWidget.goal => l10n.homeWidgetGoal,
|
HomeWidget.goal => l10n.homeWidgetGoal,
|
||||||
|
HomeWidget.pickUp => l10n.homeWidgetPickUp,
|
||||||
HomeWidget.deadlines => l10n.homeWidgetDeadlines,
|
HomeWidget.deadlines => l10n.homeWidgetDeadlines,
|
||||||
HomeWidget.week => l10n.homeWidgetWeek,
|
HomeWidget.week => l10n.homeWidgetWeek,
|
||||||
HomeWidget.activity => l10n.homeWidgetActivity,
|
HomeWidget.activity => l10n.homeWidgetActivity,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,265 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/l10n/app_localizations.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/dependencies/person_dependencies.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/core/router/route_paths.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/home/model/pick_up.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/home/model/study_activity.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/study/model/study_progress.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/study/widget/start_studying.dart';
|
||||||
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
|
import 'package:material_ui/material_ui.dart';
|
||||||
|
|
||||||
|
/// Home's Pick up learning (decided 2026-11-02, after Vaia's): the
|
||||||
|
/// studysets studied, the one studied last first, so that one studied long
|
||||||
|
/// ago sinks; [shown] of them, the others behind Show more. Each with its
|
||||||
|
/// name opening it, how much of it is learnt, when it was last studied,
|
||||||
|
/// and today's task, which starts studying it. The progress is the
|
||||||
|
/// library's, worked out once for every screen
|
||||||
|
/// (`StudysetProgressRepository`). With nothing studied it takes no room,
|
||||||
|
/// but while the dashboard is arranged ([placeholder]).
|
||||||
|
class const PickUpCard({
|
||||||
|
required final List<PickUp> pickUp,
|
||||||
|
required final DateTime now,
|
||||||
|
final bool placeholder = false,
|
||||||
|
super.key,
|
||||||
|
}) extends StatefulWidget {
|
||||||
|
/// How many show before Show more.
|
||||||
|
static const shown = 3;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PickUpCard> createState() => _PickUpCardState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PickUpCardState() extends State<PickUpCard> {
|
||||||
|
late final _progress = PersonDependencies.of(
|
||||||
|
context,
|
||||||
|
).studysetProgress.watch();
|
||||||
|
var _all = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
if (widget.pickUp.isEmpty) {
|
||||||
|
return widget.placeholder
|
||||||
|
? Card(
|
||||||
|
child: ListTile(
|
||||||
|
leading: const Icon(LucideIcons.bookOpenCheck),
|
||||||
|
title: Text(l10n.homeWidgetPickUp),
|
||||||
|
subtitle: Text(l10n.homePickUpPlaceholder),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
final listed = _all
|
||||||
|
? widget.pickUp
|
||||||
|
: widget.pickUp.take(PickUpCard.shown).toList();
|
||||||
|
final more = widget.pickUp.length - PickUpCard.shown;
|
||||||
|
return StreamBuilder<Map<String, StudyProgress>>(
|
||||||
|
stream: _progress,
|
||||||
|
builder: (context, snapshot) => Padding(
|
||||||
|
// The gap below, kept here as the reading deadlines keep theirs.
|
||||||
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(4, 0, 4, 8),
|
||||||
|
child: Text(
|
||||||
|
l10n.homeWidgetPickUp,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
for (final (index, each) in listed.indexed) ...[
|
||||||
|
if (index > 0) const SizedBox(height: 12),
|
||||||
|
_Studyset(
|
||||||
|
pickUp: each,
|
||||||
|
progress: snapshot.data?[each.studyset.id],
|
||||||
|
now: widget.now,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (more > 0)
|
||||||
|
Align(
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () => setState(() => _all = !_all),
|
||||||
|
child: Text(
|
||||||
|
_all ? l10n.homePickUpFewer : l10n.homePickUpMore(more),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One studyset to pick up: its name opening it, its learnt share as a
|
||||||
|
/// bar, when it was last studied, and today's task.
|
||||||
|
class const _Studyset({
|
||||||
|
required final PickUp pickUp,
|
||||||
|
required final StudyProgress? progress,
|
||||||
|
required final DateTime now,
|
||||||
|
}) extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colors = theme.colorScheme;
|
||||||
|
final studyset = pickUp.studyset;
|
||||||
|
final color =
|
||||||
|
PaletteColor.byName(studyset.content.colorName)?.background ??
|
||||||
|
colors.primary;
|
||||||
|
final muted = theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colors.onSurfaceVariant,
|
||||||
|
);
|
||||||
|
final today = localDay(now);
|
||||||
|
final lastDay = localDay(pickUp.lastStudied.toLocal());
|
||||||
|
// Rounded: a day with a clock change is 23 or 25 hours long.
|
||||||
|
final daysAgo = (today.difference(lastDay).inHours / 24).round();
|
||||||
|
final learned = progress?.mastery ?? 0;
|
||||||
|
return Card(
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(4, 4, 4, 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
onTap: () =>
|
||||||
|
unawaited(context.push(RoutePaths.studyset(studyset.id))),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 10, 12, 10),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
studyset.content.title,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
const Icon(LucideIcons.chevronRight, size: 18),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
LinearProgressIndicator(
|
||||||
|
value: learned,
|
||||||
|
minHeight: 6,
|
||||||
|
borderRadius: BorderRadius.circular(3),
|
||||||
|
color: color,
|
||||||
|
backgroundColor: color.withValues(alpha: 0.2),
|
||||||
|
stopIndicatorRadius: 0,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
l10n.homePickUpStudied(daysAgo < 0 ? 0 : daysAgo),
|
||||||
|
style: muted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
l10n.homePickUpLearned((learned * 100).round()),
|
||||||
|
style: muted,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
_Task(studysetId: studyset.id, progress: progress),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Today's task in a studyset: its reviews and new cards, which starts
|
||||||
|
/// studying it, outlined while reviews wait; or that nothing is due.
|
||||||
|
class const _Task({
|
||||||
|
required final String studysetId,
|
||||||
|
required final StudyProgress? progress,
|
||||||
|
}) extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colors = theme.colorScheme;
|
||||||
|
final progress = this.progress;
|
||||||
|
if (progress == null) {
|
||||||
|
return const SizedBox(height: 48);
|
||||||
|
}
|
||||||
|
final reviews = progress.reviewsDue;
|
||||||
|
final fresh = progress.due - reviews;
|
||||||
|
if (reviews == 0 && fresh == 0) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
Icon(LucideIcons.circleCheck, size: 18, color: theme.extras.success),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
l10n.homePickUpDone,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final (icon, text) = switch ((reviews, fresh)) {
|
||||||
|
(> 0, > 0) => (
|
||||||
|
LucideIcons.glasses,
|
||||||
|
l10n.homePickUpReviewAndLearn(reviews, fresh),
|
||||||
|
),
|
||||||
|
(> 0, _) => (LucideIcons.glasses, l10n.homePickUpReview(reviews)),
|
||||||
|
_ => (LucideIcons.play, l10n.homePickUpLearn(fresh)),
|
||||||
|
};
|
||||||
|
return Material(
|
||||||
|
color: colors.surfaceContainerHigh,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
// Reviews waiting stand out, as their cards slip while they wait.
|
||||||
|
side: reviews > 0
|
||||||
|
? BorderSide(color: colors.primary, width: 1.5)
|
||||||
|
: BorderSide.none,
|
||||||
|
),
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () => unawaited(_study(context)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(12, 12, 8, 12),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 20, color: colors.primary),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(child: Text(text, style: theme.textTheme.bodyLarge)),
|
||||||
|
const Icon(LucideIcons.chevronRight, size: 18),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _study(BuildContext context) async {
|
||||||
|
final progress = PersonDependencies.of(context).studysetProgress;
|
||||||
|
await startStudying(context, studysetId);
|
||||||
|
// The session's reviews changed what is due.
|
||||||
|
progress.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -183,6 +183,86 @@ class const ExamSection({required final String studysetId, super.key})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The exam on a studyset's Progress (decided 2026-11-02, after Vaia's,
|
||||||
|
/// which asks for the exam's date below its figures): with one chosen,
|
||||||
|
/// [ExamSection]'s view of how ready the person is for it; without one, an
|
||||||
|
/// invitation to choose it, opening the same sheet as the study plan's
|
||||||
|
/// row. Nothing while the plans are read.
|
||||||
|
class const ProgressExam({required final String studysetId, super.key})
|
||||||
|
extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final state = context.watch<ExamCubit>().state;
|
||||||
|
if (state.plans == null) {
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
if (state.choice != null) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 12),
|
||||||
|
child: ExamSection(studysetId: studysetId),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final l10n = AppLocalizations.of(context);
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final colors = theme.colorScheme;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 12),
|
||||||
|
child: DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
// Tinted, as Vaia's stands out from the figures above it.
|
||||||
|
color: Color.alphaBlend(
|
||||||
|
colors.primary.withValues(alpha: 0.16),
|
||||||
|
colors.surfaceContainerHigh,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
LucideIcons.calendarPlus,
|
||||||
|
size: 36,
|
||||||
|
color: colors.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
l10n.progressExamTitle,
|
||||||
|
style: theme.textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
l10n.progressExamHint,
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
FilledButton.tonalIcon(
|
||||||
|
icon: const Icon(LucideIcons.graduationCap),
|
||||||
|
label: Text(l10n.progressExamChoose),
|
||||||
|
onPressed: () => _chooseExam(context),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// How far something has come: a bar, then [label].
|
/// How far something has come: a bar, then [label].
|
||||||
class const _Bar({
|
class const _Bar({
|
||||||
required final int done,
|
required final int done,
|
||||||
|
|
|
||||||
|
|
@ -783,6 +783,13 @@ class const ProgressPart({
|
||||||
final toReview = _Tile(
|
final toReview = _Tile(
|
||||||
title: l10n.progressToReview,
|
title: l10n.progressToReview,
|
||||||
info: l10n.progressToReviewInfo,
|
info: l10n.progressToReviewInfo,
|
||||||
|
trailing: Icon(
|
||||||
|
LucideIcons.history,
|
||||||
|
size: 20,
|
||||||
|
color: progress.reviewsDue > 0
|
||||||
|
? theme.extras.warning
|
||||||
|
: colors.onSurfaceVariant,
|
||||||
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -840,8 +847,8 @@ class const ProgressPart({
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
LinearProgressIndicator(
|
LinearProgressIndicator(
|
||||||
value: learned,
|
value: learned,
|
||||||
minHeight: 8,
|
minHeight: 10,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(5),
|
||||||
// A plain track: the theme's tinted one reads as full
|
// A plain track: the theme's tinted one reads as full
|
||||||
// at 0 % in dark mode.
|
// at 0 % in dark mode.
|
||||||
backgroundColor: colors.surfaceContainerHighest,
|
backgroundColor: colors.surfaceContainerHighest,
|
||||||
|
|
@ -891,6 +898,9 @@ class const ProgressPart({
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// The exam, as Vaia asks for its date below the figures: how
|
||||||
|
// ready the person is for one chosen, else an invitation.
|
||||||
|
ProgressExam(studysetId: studysetId),
|
||||||
if (material != null && material.steps > 0) ...[
|
if (material != null && material.steps > 0) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_PathsTile(progress: material),
|
_PathsTile(progress: material),
|
||||||
|
|
@ -1091,36 +1101,45 @@ class const _Tile({
|
||||||
required final String title,
|
required final String title,
|
||||||
required final String info,
|
required final String info,
|
||||||
required final Widget child,
|
required final Widget child,
|
||||||
|
final Widget? trailing,
|
||||||
}) extends StatelessWidget {
|
}) extends StatelessWidget {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
|
// Filled, without an outline, as Vaia's (decided 2026-11-02).
|
||||||
return DecoratedBox(
|
return DecoratedBox(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: theme.colorScheme.surfaceContainer,
|
color: theme.colorScheme.surfaceContainerHigh,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(color: theme.colorScheme.outlineVariant),
|
|
||||||
),
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
|
padding: const EdgeInsets.fromLTRB(16, 14, 16, 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
// The title takes what the icon at the end leaves.
|
||||||
child: Text(title, style: theme.textTheme.titleMedium),
|
Expanded(
|
||||||
),
|
child: Row(
|
||||||
const SizedBox(width: 4),
|
children: [
|
||||||
Tooltip(
|
Flexible(
|
||||||
message: info,
|
child: Text(title, style: theme.textTheme.titleMedium),
|
||||||
triggerMode: TooltipTriggerMode.tap,
|
),
|
||||||
child: Icon(
|
const SizedBox(width: 6),
|
||||||
LucideIcons.info,
|
Tooltip(
|
||||||
size: 16,
|
message: info,
|
||||||
color: theme.colorScheme.onSurfaceVariant,
|
triggerMode: TooltipTriggerMode.tap,
|
||||||
|
child: Icon(
|
||||||
|
LucideIcons.info,
|
||||||
|
size: 16,
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
?trailing,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,12 @@ void main() {
|
||||||
(s) => s.goal != null && s.layout.hidden.isNotEmpty,
|
(s) => s.goal != null && s.layout.hidden.isNotEmpty,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(state.layout.shown, [HomeWidget.week, HomeWidget.goal]);
|
// Pick up learning, which the layout missed, comes after the goal.
|
||||||
|
expect(state.layout.shown, [
|
||||||
|
HomeWidget.week,
|
||||||
|
HomeWidget.goal,
|
||||||
|
HomeWidget.pickUp,
|
||||||
|
]);
|
||||||
expect((await settings.read()).goal?.target, 50);
|
expect((await settings.read()).goal?.target, 50);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/home/model/pick_up.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/study/model/rating.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/study/model/review.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
Studyset studyset(String id, {String? parent, DateTime? archivedAt}) =>
|
||||||
|
Studyset(
|
||||||
|
id: id,
|
||||||
|
content: StudysetContent.titled(id),
|
||||||
|
parentId: parent,
|
||||||
|
archivedAt: archivedAt,
|
||||||
|
);
|
||||||
|
|
||||||
|
Review rated(String card, DateTime at, {StudyMode mode = StudyMode.review}) =>
|
||||||
|
Review(
|
||||||
|
id: '$card-$at',
|
||||||
|
cardId: card,
|
||||||
|
rating: Rating.good,
|
||||||
|
reviewedAt: at,
|
||||||
|
duration: Duration.zero,
|
||||||
|
mode: mode,
|
||||||
|
);
|
||||||
|
|
||||||
|
test('the studysets studied, the one studied last first, each counting '
|
||||||
|
'what was studied in its topics; one never studied is left out', () {
|
||||||
|
final studysets = [
|
||||||
|
studyset('japanese'),
|
||||||
|
studyset('kana', parent: 'japanese'),
|
||||||
|
studyset('latin'),
|
||||||
|
studyset('psychology'),
|
||||||
|
];
|
||||||
|
final picked = pickUpOf(
|
||||||
|
studysets,
|
||||||
|
{'neko': 'japanese', 'a': 'kana', 'amo': 'latin'},
|
||||||
|
[
|
||||||
|
rated('neko', DateTime.utc(2026)),
|
||||||
|
rated('amo', DateTime.utc(2026, 10)),
|
||||||
|
// Studied in a topic: the studyset counts as studied then.
|
||||||
|
rated('a', DateTime.utc(2026, 11)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
[for (final each in picked) each.studyset.id],
|
||||||
|
[
|
||||||
|
'japanese',
|
||||||
|
'latin',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
expect(picked.first.lastStudied, DateTime.utc(2026, 11));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a fresh start is no studying, and an archived studyset rests', () {
|
||||||
|
final picked = pickUpOf(
|
||||||
|
[
|
||||||
|
studyset('japanese'),
|
||||||
|
studyset('latin', archivedAt: DateTime.utc(2026, 5)),
|
||||||
|
],
|
||||||
|
{'neko': 'japanese', 'amo': 'latin'},
|
||||||
|
[
|
||||||
|
rated('neko', DateTime.utc(2026, 11), mode: StudyMode.reset),
|
||||||
|
rated('amo', DateTime.utc(2026, 11)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(picked, isEmpty);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -7,6 +7,10 @@ import 'package:halkyon_learn_flutter/src/feature/home/widget/streak.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/pomodoro/widget/pomodoro_card.dart';
|
import 'package:halkyon_learn_flutter/src/feature/pomodoro/widget/pomodoro_card.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
|
import 'package:halkyon_learn_flutter/src/feature/study/data/review_repository.dart';
|
||||||
import 'package:halkyon_learn_flutter/src/feature/study/model/rating.dart';
|
import 'package:halkyon_learn_flutter/src/feature/study/model/rating.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/study/widget/study_screen.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/studyset/data/studyset_payload.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/studyset/model/studyset_content.dart';
|
||||||
|
import 'package:halkyon_learn_flutter/src/feature/studyset/widget/studyset_screen.dart';
|
||||||
import 'package:halkyon_ui/halkyon_ui.dart';
|
import 'package:halkyon_ui/halkyon_ui.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
import 'package:lucide_icons_flutter/lucide_icons.dart';
|
||||||
|
|
@ -15,6 +19,7 @@ import 'package:material_ui/material_ui.dart';
|
||||||
import '../../../support/fake_card_gateway.dart';
|
import '../../../support/fake_card_gateway.dart';
|
||||||
import '../../../support/fake_review_gateway.dart';
|
import '../../../support/fake_review_gateway.dart';
|
||||||
import '../../../support/fake_settings_gateway.dart';
|
import '../../../support/fake_settings_gateway.dart';
|
||||||
|
import '../../../support/fake_studyset_gateway.dart';
|
||||||
import '../../../support/pump_app.dart';
|
import '../../../support/pump_app.dart';
|
||||||
import '../../../support/test_dependencies.dart';
|
import '../../../support/test_dependencies.dart';
|
||||||
|
|
||||||
|
|
@ -22,8 +27,10 @@ void main() {
|
||||||
late FakeCardGateway cardGateway;
|
late FakeCardGateway cardGateway;
|
||||||
late FakeReviewGateway reviewGateway;
|
late FakeReviewGateway reviewGateway;
|
||||||
late FakeSettingsGateway settingsGateway;
|
late FakeSettingsGateway settingsGateway;
|
||||||
|
late FakeStudysetGateway studysetGateway;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
|
studysetGateway = FakeStudysetGateway();
|
||||||
cardGateway = FakeCardGateway();
|
cardGateway = FakeCardGateway();
|
||||||
reviewGateway = FakeReviewGateway();
|
reviewGateway = FakeReviewGateway();
|
||||||
settingsGateway = FakeSettingsGateway();
|
settingsGateway = FakeSettingsGateway();
|
||||||
|
|
@ -36,6 +43,7 @@ void main() {
|
||||||
await pumpApp(
|
await pumpApp(
|
||||||
tester,
|
tester,
|
||||||
testDependencies(
|
testDependencies(
|
||||||
|
studysetGateway: studysetGateway,
|
||||||
cardGateway: cardGateway,
|
cardGateway: cardGateway,
|
||||||
reviewGateway: reviewGateway,
|
reviewGateway: reviewGateway,
|
||||||
settingsGateway: settingsGateway,
|
settingsGateway: settingsGateway,
|
||||||
|
|
@ -46,22 +54,113 @@ void main() {
|
||||||
int streak(WidgetTester tester) =>
|
int streak(WidgetTester tester) =>
|
||||||
tester.widget<StreakChip>(find.byType(StreakChip)).streak;
|
tester.widget<StreakChip>(find.byType(StreakChip)).streak;
|
||||||
|
|
||||||
/// Rates a new card now.
|
/// Rates a new card in [studyset] at [at], now by default.
|
||||||
Future<void> rateToday() async {
|
Future<void> rateToday({String studyset = 'japanese', DateTime? at}) async {
|
||||||
final cards = CardRepository(cardGateway);
|
final cards = CardRepository(cardGateway);
|
||||||
addTearDown(cards.dispose);
|
addTearDown(cards.dispose);
|
||||||
final id = await cards.create(
|
final id = await cards.create(
|
||||||
'japanese',
|
studyset,
|
||||||
CardContent(front: RichContent.empty(), back: RichContent.empty()),
|
CardContent(front: RichContent.empty(), back: RichContent.empty()),
|
||||||
);
|
);
|
||||||
await ReviewRepository(reviewGateway).record(
|
await ReviewRepository(reviewGateway).record(
|
||||||
cardId: id,
|
cardId: id,
|
||||||
rating: Rating.good,
|
rating: Rating.good,
|
||||||
reviewedAt: DateTime.now(),
|
reviewedAt: at ?? DateTime.now(),
|
||||||
duration: Duration.zero,
|
duration: Duration.zero,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The studyset [id], titled [title], on this device.
|
||||||
|
void stored(String id, String title) => studysetGateway.receiveRemoteChange(
|
||||||
|
id,
|
||||||
|
encodeStudysetPayload(StudysetContent.titled(title)),
|
||||||
|
);
|
||||||
|
|
||||||
|
/// A card never studied in [studyset].
|
||||||
|
Future<void> newCard(String studyset) async {
|
||||||
|
final cards = CardRepository(cardGateway);
|
||||||
|
addTearDown(cards.dispose);
|
||||||
|
await cards.create(
|
||||||
|
studyset,
|
||||||
|
CardContent(front: RichContent.empty(), back: RichContent.empty()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
group('Pick up learning', () {
|
||||||
|
testWidgets('lists the studysets studied, the one studied last first, '
|
||||||
|
"with how much is learnt and today's task, which starts studying "
|
||||||
|
'it; one never studied is left out', (tester) async {
|
||||||
|
stored('japanese', 'Japanese');
|
||||||
|
stored('latin', 'Latin');
|
||||||
|
stored('psychology', 'Psychology');
|
||||||
|
await rateToday(
|
||||||
|
studyset: 'latin',
|
||||||
|
at: DateTime.now().subtract(const Duration(days: 3)),
|
||||||
|
);
|
||||||
|
await rateToday();
|
||||||
|
await newCard('japanese');
|
||||||
|
await newCard('psychology');
|
||||||
|
await showHome(tester);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Pick up learning'), findsOneWidget);
|
||||||
|
expect(find.text('Psychology'), findsNothing);
|
||||||
|
expect(
|
||||||
|
tester.getTopLeft(find.text('Japanese')).dy,
|
||||||
|
lessThan(tester.getTopLeft(find.text('Latin')).dy),
|
||||||
|
);
|
||||||
|
expect(find.text('Studied today'), findsOneWidget);
|
||||||
|
expect(find.text('Studied 3 days ago'), findsOneWidget);
|
||||||
|
expect(find.text('Learn 1 new card'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Learn 1 new card'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.byType(StudyScreen), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets("a studyset's name opens it", (tester) async {
|
||||||
|
stored('japanese', 'Japanese');
|
||||||
|
await rateToday();
|
||||||
|
await showHome(tester);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.text('Japanese'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.byType(StudysetScreen), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows three, the others behind Show more', (tester) async {
|
||||||
|
for (final (days, name) in [
|
||||||
|
(1, 'One'),
|
||||||
|
(2, 'Two'),
|
||||||
|
(3, 'Three'),
|
||||||
|
(4, 'Four'),
|
||||||
|
]) {
|
||||||
|
stored(name, name);
|
||||||
|
await rateToday(
|
||||||
|
studyset: name,
|
||||||
|
at: DateTime.now().subtract(Duration(days: days)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
await showHome(tester);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Four'), findsNothing);
|
||||||
|
await tester.tap(find.text('Show 1 more'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Four'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('with nothing studied yet, it takes no room', (tester) async {
|
||||||
|
stored('japanese', 'Japanese');
|
||||||
|
await newCard('japanese');
|
||||||
|
await showHome(tester);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Pick up learning'), findsNothing);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('cobalt washes down from the top, behind a clear app bar', (
|
testWidgets('cobalt washes down from the top, behind a clear app bar', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
|
|
@ -246,6 +345,7 @@ void main() {
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
for (final name in [
|
for (final name in [
|
||||||
'Daily goal',
|
'Daily goal',
|
||||||
|
'Pick up learning',
|
||||||
'Pomodoro',
|
'Pomodoro',
|
||||||
'PDF reading deadlines',
|
'PDF reading deadlines',
|
||||||
'Week overview',
|
'Week overview',
|
||||||
|
|
|
||||||
|
|
@ -160,6 +160,7 @@ void main() {
|
||||||
order: [
|
order: [
|
||||||
HomeWidget.activity,
|
HomeWidget.activity,
|
||||||
HomeWidget.goal,
|
HomeWidget.goal,
|
||||||
|
HomeWidget.pickUp,
|
||||||
HomeWidget.pomodoro,
|
HomeWidget.pomodoro,
|
||||||
HomeWidget.deadlines,
|
HomeWidget.deadlines,
|
||||||
HomeWidget.week,
|
HomeWidget.week,
|
||||||
|
|
@ -172,7 +173,8 @@ void main() {
|
||||||
});
|
});
|
||||||
|
|
||||||
test('a home layout from another version keeps the widgets this one '
|
test('a home layout from another version keeps the widgets this one '
|
||||||
'knows, and adds those it misses', () {
|
'knows, and adds those it misses, each after the one a new dashboard '
|
||||||
|
'has before it', () {
|
||||||
final settings = decodeSettingsPayload(
|
final settings = decodeSettingsPayload(
|
||||||
encodePayload(3, {
|
encodePayload(3, {
|
||||||
'studyTags': {},
|
'studyTags': {},
|
||||||
|
|
@ -184,15 +186,39 @@ void main() {
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(settings.home.order, [
|
expect(settings.home.order, [
|
||||||
HomeWidget.activity,
|
|
||||||
HomeWidget.goal,
|
HomeWidget.goal,
|
||||||
|
HomeWidget.pickUp,
|
||||||
HomeWidget.pomodoro,
|
HomeWidget.pomodoro,
|
||||||
HomeWidget.deadlines,
|
HomeWidget.deadlines,
|
||||||
HomeWidget.week,
|
HomeWidget.week,
|
||||||
|
HomeWidget.activity,
|
||||||
]);
|
]);
|
||||||
expect(settings.home.hidden, isEmpty);
|
expect(settings.home.hidden, isEmpty);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a dashboard arranged before Pick up learning gets it below the '
|
||||||
|
'goal, wherever the goal is', () {
|
||||||
|
final settings = decodeSettingsPayload(
|
||||||
|
encodePayload(3, {
|
||||||
|
'studyTags': {},
|
||||||
|
'home': {
|
||||||
|
'order': ['week', 'activity', 'goal', 'pomodoro', 'deadlines'],
|
||||||
|
'hidden': ['pomodoro'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(settings.home.order, [
|
||||||
|
HomeWidget.week,
|
||||||
|
HomeWidget.activity,
|
||||||
|
HomeWidget.goal,
|
||||||
|
HomeWidget.pickUp,
|
||||||
|
HomeWidget.pomodoro,
|
||||||
|
HomeWidget.deadlines,
|
||||||
|
]);
|
||||||
|
expect(settings.home.hidden, {HomeWidget.pomodoro});
|
||||||
|
});
|
||||||
|
|
||||||
test('folding keeps both rest days, and the goal one of them has', () {
|
test('folding keeps both rest days, and the goal one of them has', () {
|
||||||
final mine = PersonSettings(restDays: {DateTime(2026, 10, 5)});
|
final mine = PersonSettings(restDays: {DateTime(2026, 10, 5)});
|
||||||
final theirs = PersonSettings(
|
final theirs = PersonSettings(
|
||||||
|
|
|
||||||
|
|
@ -2051,6 +2051,35 @@ void main() {
|
||||||
expect(find.text('1 min'), findsOneWidget);
|
expect(find.text('1 min'), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('Progress invites choosing the exam below its figures, as '
|
||||||
|
'Vaia asks for its date; chosen, it shows how ready the person is', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
stored('japanese', 'Japanese');
|
||||||
|
final neko = await card('japanese', '猫');
|
||||||
|
await showApp(tester, location: RoutePaths.studyset('japanese'));
|
||||||
|
final person = PersonDependencies.of(
|
||||||
|
tester.element(find.byType(StudysetScreen)),
|
||||||
|
);
|
||||||
|
final n5 = await person.tags.create('JLPT-N5');
|
||||||
|
await person.tags.setTagsOf(neko, {n5});
|
||||||
|
await tester.pump(ExamCubit.settle);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await openPart(tester, 'Progress');
|
||||||
|
expect(find.text('Preparing for an exam?'), findsOneWidget);
|
||||||
|
await tester.tap(find.text('Choose the exam'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('JLPT N5'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Save'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.text('Preparing for an exam?'), findsNothing);
|
||||||
|
expect(find.textContaining('In 84 days'), findsOneWidget);
|
||||||
|
expect(find.text('0 of 1 flashcard started'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('Progress also counts the steps of the learning paths done, '
|
testWidgets('Progress also counts the steps of the learning paths done, '
|
||||||
'and lists the PDFs not read to their end, begun first, across the '
|
'and lists the PDFs not read to their end, begun first, across the '
|
||||||
'topics; one opens where it is read', (tester) async {
|
'topics; one opens where it is read', (tester) async {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue