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
20 changes: 20 additions & 0 deletions lib/services/dictionary_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:archive/archive_io.dart' as archive;
import 'package:sagase/datamodels/user_backup.dart';
import 'package:sagase/services/isar_service.dart';
import 'package:sagase/utils/constants.dart' as constants;
import 'package:sagase/utils/date_time_utils.dart';
import 'package:sagase_dictionary/sagase_dictionary.dart';
import 'package:path/path.dart' as path;

Expand Down Expand Up @@ -465,6 +466,25 @@ class DictionaryService {
);
}

Future<void> spaceOutFlashcards(List<DictionaryItem> flashcards) async {
await _database.transaction(() async {
final now = DateTime.now();

int flashcardsPerDay = (flashcards.length - 150) ~/ 12;
for (int i = 1; i < 14; i++) {
int dueDate = now.add(Duration(days: i)).toInt();
for (int j = 0; j < flashcardsPerDay && flashcards.length > 150; j++) {
await setSpacedRepetitionData(
flashcards
.removeLast()
.spacedRepetitionData!
.copyWith(dueDate: dueDate),
);
}
}
});
}

Future<void> setSpacedRepetitionData(SpacedRepetitionData data) async {
return _database.spacedRepetitionDatasDao.set(data);
}
Expand Down
10 changes: 10 additions & 0 deletions lib/services/shared_preferences_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,14 @@ class SharedPreferencesService implements InitializableDependency {
Future<void> setProperNounsEnabled(bool value) async {
await _sharedPreferences.setBool(constants.keyProperNounsEnabled, value);
}

bool getAddNewFlashcardsInBatches() {
return _sharedPreferences.getBool(constants.keyAddNewFlashcardsInBatches) ??
constants.defaultAddNewFlashcardsInBatches;
}

Future<void> setAddNewFlashcardsInBatches(bool value) async {
await _sharedPreferences.setBool(
constants.keyAddNewFlashcardsInBatches, value);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,18 @@ class FlashcardSetSettingsView
if (flashcardSet.usingSpacedRepetition)
const PopupMenuItem(
value: PopupMenuItemType.reset,
child: Text('Reset'),
child: Text('Reset progress'),
),
if (flashcardSet.usingSpacedRepetition)
const PopupMenuItem(
value: PopupMenuItemType.statistics,
child: Text('View statistics'),
),
if (flashcardSet.usingSpacedRepetition)
const PopupMenuItem(
value: PopupMenuItemType.spaceOut,
child: Text('Space out flashcards'),
),
],
onSelected: viewModel.handlePopupMenuButton,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:sagase/app/app.locator.dart';
import 'package:sagase/app/app.router.dart';
import 'package:sagase/datamodels/lists_bottom_sheet_argument.dart';
import 'package:sagase/services/shared_preferences_service.dart';
import 'package:sagase/utils/date_time_utils.dart';
import 'package:sagase_dictionary/sagase_dictionary.dart';
import 'package:sagase/datamodels/my_lists_bottom_sheet_item.dart';
import 'package:sagase/services/dictionary_service.dart';
Expand Down Expand Up @@ -50,6 +51,9 @@ class FlashcardSetSettingsViewModel extends FutureViewModel {
case PopupMenuItemType.statistics:
_openFlashcardSetInfo();
break;
case PopupMenuItemType.spaceOut:
_spaceOutDueFlashcards();
break;
}
}

Expand Down Expand Up @@ -245,11 +249,50 @@ class FlashcardSetSettingsViewModel extends FutureViewModel {
bool shouldShowTutorial() {
return _sharedPreferencesService.getAndSetTutorialFlashcardSetSettings();
}

Future<void> _spaceOutDueFlashcards() async {
final response = await _dialogService.showCustomDialog(
variant: DialogType.confirmation,
title: 'Reduce due flashcards?',
description:
'Have too many due flashcards? To help you study due flashcards can be delayed. Pressing confirm will leave 150 due flashcards for today and spread the rest out over the next 2 weeks.',
mainButtonTitle: 'Confirm',
secondaryButtonTitle: 'Cancel',
barrierDismissible: true,
);

if (response != null && response.confirmed) {
_dialogService.showCustomDialog(
variant: DialogType.progressIndicator,
title: 'Updating flashcards',
barrierDismissible: false,
);

final allFlashcards =
await _dictionaryService.getFlashcardSetFlashcards(flashcardSet);
final dueFlashcards = <DictionaryItem>[];

final sessionDateTime = DateTime.now();
int todayAsInt = sessionDateTime.toInt();
for (var item in allFlashcards) {
if (item.spacedRepetitionData != null &&
item.spacedRepetitionData!.dueDate! <= todayAsInt) {
dueFlashcards.add(item);
}
}

dueFlashcards.shuffle();
await _dictionaryService.spaceOutFlashcards(dueFlashcards);

_dialogService.completeDialog(DialogResponse());
}
}
}

enum PopupMenuItemType {
rename,
delete,
reset,
statistics,
spaceOut,
}
39 changes: 20 additions & 19 deletions lib/ui/views/flashcards/flashcards_viewmodel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -344,11 +344,26 @@ class FlashcardsViewModel extends FutureViewModel {
// If active flashcards is still empty then try to add new flashcards
Future<DialogResponse<dynamic>?>? reportDialogResponse;
if (activeFlashcards.isEmpty) {
activeFlashcards.addAll(newFlashcards);
newFlashcards.clear();
if (_sharedPreferencesService.getFlashcardLearningModeEnabled() &&
_sharedPreferencesService.getAddNewFlashcardsInBatches()) {
if (initial) newFlashcards.shuffle(_random);

int newFlashcardsToAdd = max(
0,
min(
_sharedPreferencesService.getNewFlashcardsPerDay() -
startedFlashcards.length,
newFlashcards.length,
));
activeFlashcards.addAll(newFlashcards.take(newFlashcardsToAdd));
newFlashcards.removeRange(0, newFlashcardsToAdd);
} else {
activeFlashcards.addAll(newFlashcards);
newFlashcards.clear();
activeFlashcards.shuffle(_random);
}
_answeringDueFlashcards = false;
// Randomize
activeFlashcards.shuffle(_random);

// Add any started flashcards
activeFlashcards.insertAll(0, startedFlashcards..shuffle(_random));
startedFlashcards.clear();
Expand Down Expand Up @@ -716,28 +731,14 @@ class FlashcardsViewModel extends FutureViewModel {
);

if (response != null && response.confirmed) {
// Show progress indicator dialog
_dialogService.showCustomDialog(
variant: DialogType.progressIndicator,
title: 'Updating flashcards',
barrierDismissible: false,
);

dueFlashcards.shuffle(_random);
int flashcardsPerDay = (dueFlashcards.length - 150) ~/ 12;
for (int i = 1; i < 14; i++) {
int dueDate = sessionDateTime.add(Duration(days: i)).toInt();
for (int j = 0;
j < flashcardsPerDay && dueFlashcards.length > 150;
j++) {
await _dictionaryService.setSpacedRepetitionData(
dueFlashcards
.removeLast()
.spacedRepetitionData!
.copyWith(dueDate: dueDate),
);
}
}
await _dictionaryService.spaceOutFlashcards(dueFlashcards);

_dialogService.completeDialog(DialogResponse());
}
Expand Down
10 changes: 10 additions & 0 deletions lib/ui/views/settings/settings_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,16 @@ class SettingsView extends StatelessWidget {
),
onPressed: (_) => viewModel.setNewFlashcardsPerDay(),
),
SettingsTile.switchTile(
enabled: viewModel.flashcardLearningModeEnabled,
initialValue: viewModel.addNewFlashcardsInBatches,
onToggle: viewModel.setAddNewFlashcardsInBatches,
activeSwitchColor: Theme.of(context).colorScheme.primary,
title: const Text('Add new flashcards in batches'),
description: const Text(
'If enabled, when no due flashcards are available new flashcards are added in batches instead of all at once.',
),
),
SettingsTile.navigation(
title:
const Text('Set initial spaced repetition interval'),
Expand Down
7 changes: 7 additions & 0 deletions lib/ui/views/settings/settings_viewmodel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class SettingsViewModel extends BaseViewModel {
_sharedPreferencesService.getFlashcardLearningModeEnabled();
int get newFlashcardsPerDay =>
_sharedPreferencesService.getNewFlashcardsPerDay();
bool get addNewFlashcardsInBatches =>
_sharedPreferencesService.getAddNewFlashcardsInBatches();
int get flashcardDistance => _sharedPreferencesService.getFlashcardDistance();
int get flashcardCorrectAnswersRequired =>
_sharedPreferencesService.getFlashcardCorrectAnswersRequired();
Expand Down Expand Up @@ -107,6 +109,11 @@ class SettingsViewModel extends BaseViewModel {
} catch (_) {}
}

void setAddNewFlashcardsInBatches(bool value) {
_sharedPreferencesService.setAddNewFlashcardsInBatches(value);
notifyListeners();
}

Future<void> setFlashcardDistance() async {
final response = await _dialogService.showCustomDialog(
variant: DialogType.numberTextField,
Expand Down
2 changes: 2 additions & 0 deletions lib/utils/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const keyTutorialVocab = 'tutorial_vocab';
const keyShowDetailedProgress = 'show_detailed_progress';
const keyChangelogVersionShown = 'changelog_version_shown';
const keyProperNounsEnabled = 'proper_nouns_enabled';
const keyAddNewFlashcardsInBatches = 'add_new_flashcards_in_batches';

const defaultInitialCorrectInterval = 1;
const defaultInitialVeryCorrectInterval = 4;
Expand All @@ -40,6 +41,7 @@ const defaultStartOnLearningView = false;
const defaultStrokeDiagramStartExpanded = true;
const defaultShowDetailedProgress = false;
const defaultProperNounsEnabled = false;
const defaultAddNewFlashcardsInBatches = false;

const searchQueryLimit = 1000;

Expand Down
4 changes: 4 additions & 0 deletions test/helpers/mocks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,8 @@ MockSharedPreferencesService getAndRegisterSharedPreferencesService({
bool getShowDetailedProgress = constants.defaultShowDetailedProgress,
bool getOnboardingFinished = false,
bool getProperNounsEnabled = constants.defaultProperNounsEnabled,
bool getAddNewFlashcardsInBatches =
constants.defaultAddNewFlashcardsInBatches,
}) {
_removeRegistrationIfExists<SharedPreferencesService>();
final service = MockSharedPreferencesService();
Expand All @@ -188,6 +190,8 @@ MockSharedPreferencesService getAndRegisterSharedPreferencesService({
when(service.getShowDetailedProgress()).thenReturn(getShowDetailedProgress);
when(service.getOnboardingFinished()).thenReturn(getOnboardingFinished);
when(service.getProperNounsEnabled()).thenReturn(getProperNounsEnabled);
when(service.getAddNewFlashcardsInBatches())
.thenReturn(getAddNewFlashcardsInBatches);

locator.registerSingleton<SharedPreferencesService>(service);
return service;
Expand Down
32 changes: 32 additions & 0 deletions test/ui/views/flashcards/flashcards_viewmodel_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1981,5 +1981,37 @@ void main() {
await dictionaryService.getRecentFlashcardSetReport(flashcardSet);
expect(report, null);
});

test(
'Add new flashcards in batches when enabled and have no due flashcards',
() async {
// Set shared preferences
getAndRegisterSharedPreferencesService(
getFlashcardLearningModeEnabled: true,
getNewFlashcardsPerDay: 1,
getAddNewFlashcardsInBatches: true,
);

// Create dictionary lists to use
final dictionaryList =
await dictionaryService.createMyDictionaryList('list1');
await dictionaryService.addToMyDictionaryList(
dictionaryList, getVocab1());
await dictionaryService.addToMyDictionaryList(
dictionaryList, getVocab2());

// Create flashcard set and assign lists
final flashcardSet = await dictionaryService.createFlashcardSet('name');
flashcardSet.myDictionaryLists.add(dictionaryList.id);
await dictionaryService.updateFlashcardSet(flashcardSet);

// Call initialize
var viewModel = FlashcardsViewModel(flashcardSet, null, randomSeed: 123);
await viewModel.futureToRun();

// Check results
expect(viewModel.activeFlashcards.length, 1);
expect(viewModel.newFlashcards.length, 1);
});
});
}