diff --git a/assets/exercises/day1_en.json b/assets/exercises/day1_en.json new file mode 100644 index 0000000..0f53276 --- /dev/null +++ b/assets/exercises/day1_en.json @@ -0,0 +1,83 @@ +{ + "exercises": [ + { + "id": "day1_mc1", + "type": "multipleChoice", + "question": "Which greeting is most appropriate in a formal setting?", + "options": [ + "Hey!", + "Good morning", + "Yo!", + "What's up?" + ], + "correctOptionIndex": 1 + }, + { + "id": "day1_mc2", + "type": "multipleChoice", + "question": "How do you respond to 'How are you?'", + "options": [ + "I'm fine, thank you. And you?", + "Nothing", + "Yes", + "Goodbye" + ], + "correctOptionIndex": 0 + }, + { + "id": "day1_fib1", + "type": "fillInBlank", + "question": "Fill in the blank: 'Nice to ____ you!'", + "correctAnswer": "meet", + "caseSensitive": false + }, + { + "id": "day1_fib2", + "type": "fillInBlank", + "question": "Fill in the blank: 'My ____ is John.'", + "correctAnswer": "name", + "acceptableAlternatives": ["Name"], + "caseSensitive": false + }, + { + "id": "day1_trans1", + "type": "translation", + "question": "Translate to English:", + "targetText": "Hello", + "correctTranslation": "Hello", + "acceptableAlternatives": ["Hi", "Hey", "Greetings"] + }, + { + "id": "day1_mc3", + "type": "multipleChoice", + "question": "What is an appropriate way to say goodbye?", + "options": [ + "See you later", + "Go away", + "Stop talking", + "Leave now" + ], + "correctOptionIndex": 0 + }, + { + "id": "day1_fib3", + "type": "fillInBlank", + "question": "Fill in the blank: '____ to meet you!'", + "correctAnswer": "Nice", + "acceptableAlternatives": ["Pleased", "Happy", "Good"], + "caseSensitive": false + }, + { + "id": "day1_mc4", + "type": "multipleChoice", + "question": "Which is a polite way to introduce yourself?", + "options": [ + "I'm the best", + "My name is Sarah", + "You should know me", + "I don't care" + ], + "correctOptionIndex": 1 + } + ] +} diff --git a/assets/exercises/day2_en.json b/assets/exercises/day2_en.json new file mode 100644 index 0000000..dd73f0c --- /dev/null +++ b/assets/exercises/day2_en.json @@ -0,0 +1,69 @@ +{ + "exercises": [ + { + "id": "day2_mc1", + "type": "multipleChoice", + "question": "What comes after 'nine'?", + "options": [ + "eight", + "ten", + "eleven", + "twelve" + ], + "correctOptionIndex": 1 + }, + { + "id": "day2_fib1", + "type": "fillInBlank", + "question": "Fill in the blank: 'One, two, three, ____, five'", + "correctAnswer": "four", + "caseSensitive": false + }, + { + "id": "day2_mc2", + "type": "multipleChoice", + "question": "How do you write the number 15?", + "options": [ + "fiveteen", + "fifty", + "fifteen", + "fivetin" + ], + "correctOptionIndex": 2 + }, + { + "id": "day2_fib2", + "type": "fillInBlank", + "question": "What number comes before 'twenty'?", + "correctAnswer": "nineteen", + "caseSensitive": false + }, + { + "id": "day2_mc3", + "type": "multipleChoice", + "question": "Which is the correct spelling of 30?", + "options": [ + "thirthy", + "thirty", + "therty", + "threety" + ], + "correctOptionIndex": 1 + }, + { + "id": "day2_trans1", + "type": "translation", + "question": "Write this number in words:", + "targetText": "7", + "correctTranslation": "seven", + "acceptableAlternatives": ["Seven"] + }, + { + "id": "day2_fib3", + "type": "fillInBlank", + "question": "Fill in the blank: 'Ten, twenty, ____, forty'", + "correctAnswer": "thirty", + "caseSensitive": false + } + ] +} diff --git a/assets/exercises/day3_en.json b/assets/exercises/day3_en.json new file mode 100644 index 0000000..27a1d44 --- /dev/null +++ b/assets/exercises/day3_en.json @@ -0,0 +1,82 @@ +{ + "exercises": [ + { + "id": "day3_mc1", + "type": "multipleChoice", + "question": "Which day comes after Monday?", + "options": [ + "Sunday", + "Tuesday", + "Wednesday", + "Saturday" + ], + "correctOptionIndex": 1 + }, + { + "id": "day3_mc2", + "type": "multipleChoice", + "question": "What is the first month of the year?", + "options": [ + "December", + "February", + "January", + "March" + ], + "correctOptionIndex": 2 + }, + { + "id": "day3_fib1", + "type": "fillInBlank", + "question": "Fill in the blank: 'Monday, Tuesday, ____'", + "correctAnswer": "Wednesday", + "caseSensitive": false + }, + { + "id": "day3_fib2", + "type": "fillInBlank", + "question": "Fill in the blank: 'January, February, ____'", + "correctAnswer": "March", + "caseSensitive": false + }, + { + "id": "day3_mc3", + "type": "multipleChoice", + "question": "Which month has the shortest name?", + "options": [ + "July", + "June", + "May", + "April" + ], + "correctOptionIndex": 2 + }, + { + "id": "day3_trans1", + "type": "translation", + "question": "What day is the last day of the work week?", + "targetText": "Last work day before weekend", + "correctTranslation": "Friday", + "acceptableAlternatives": ["friday"] + }, + { + "id": "day3_mc4", + "type": "multipleChoice", + "question": "What are the weekend days?", + "options": [ + "Monday and Friday", + "Saturday and Sunday", + "Thursday and Friday", + "Tuesday and Wednesday" + ], + "correctOptionIndex": 1 + }, + { + "id": "day3_fib3", + "type": "fillInBlank", + "question": "Fill in the blank: 'Thursday, Friday, ____'", + "correctAnswer": "Saturday", + "acceptableAlternatives": ["saturday"], + "caseSensitive": false + } + ] +} diff --git a/assets/vocabulary/day1_en.json b/assets/vocabulary/day1_en.json new file mode 100644 index 0000000..5ef9b47 --- /dev/null +++ b/assets/vocabulary/day1_en.json @@ -0,0 +1,60 @@ +{ + "vocabulary": [ + { + "word": "Hello", + "translation": "A greeting used when meeting someone", + "phonetic": "/həˈloʊ/", + "example": "Hello, how are you today?", + "exampleTranslation": "A friendly greeting to start a conversation" + }, + { + "word": "Goodbye", + "translation": "A farewell expression", + "phonetic": "/ɡʊdˈbaɪ/", + "example": "Goodbye, see you tomorrow!", + "exampleTranslation": "A polite way to end a conversation" + }, + { + "word": "Please", + "translation": "A polite word used when making requests", + "phonetic": "/pliːz/", + "example": "Could you help me, please?", + "exampleTranslation": "Shows politeness when asking for something" + }, + { + "word": "Thank you", + "translation": "Expression of gratitude", + "phonetic": "/θæŋk juː/", + "example": "Thank you for your help!", + "exampleTranslation": "Showing appreciation for someone's actions" + }, + { + "word": "My name is", + "translation": "Phrase used to introduce yourself", + "phonetic": "/maɪ neɪm ɪz/", + "example": "Hi, my name is Sarah.", + "exampleTranslation": "Introducing yourself to someone new" + }, + { + "word": "Nice to meet you", + "translation": "Polite expression when meeting someone for the first time", + "phonetic": "/naɪs tə miːt juː/", + "example": "Nice to meet you, John!", + "exampleTranslation": "A friendly greeting for first encounters" + }, + { + "word": "How are you?", + "translation": "A common question to ask about someone's well-being", + "phonetic": "/haʊ ɑːr juː/", + "example": "Hello! How are you doing today?", + "exampleTranslation": "Asking about someone's current state" + }, + { + "word": "I'm fine", + "translation": "A common response indicating you are well", + "phonetic": "/aɪm faɪn/", + "example": "I'm fine, thank you. And you?", + "exampleTranslation": "Positive response to 'How are you?'" + } + ] +} diff --git a/assets/vocabulary/day2_en.json b/assets/vocabulary/day2_en.json new file mode 100644 index 0000000..0be7215 --- /dev/null +++ b/assets/vocabulary/day2_en.json @@ -0,0 +1,60 @@ +{ + "vocabulary": [ + { + "word": "Zero", + "translation": "The number 0", + "phonetic": "/ˈzɪroʊ/", + "example": "I have zero apples.", + "exampleTranslation": "Indicating the absence of quantity" + }, + { + "word": "One", + "translation": "The number 1", + "phonetic": "/wʌn/", + "example": "I need one ticket.", + "exampleTranslation": "A single item" + }, + { + "word": "Five", + "translation": "The number 5", + "phonetic": "/faɪv/", + "example": "There are five books on the table.", + "exampleTranslation": "Counting to five items" + }, + { + "word": "Ten", + "translation": "The number 10", + "phonetic": "/tɛn/", + "example": "I wake up at ten o'clock.", + "exampleTranslation": "The number after nine" + }, + { + "word": "Twenty", + "translation": "The number 20", + "phonetic": "/ˈtwɛnti/", + "example": "The book costs twenty dollars.", + "exampleTranslation": "Two tens" + }, + { + "word": "Hundred", + "translation": "The number 100", + "phonetic": "/ˈhʌndrəd/", + "example": "One hundred people attended.", + "exampleTranslation": "Ten times ten" + }, + { + "word": "First", + "translation": "Position number 1 in a sequence", + "phonetic": "/fɜːrst/", + "example": "She finished first in the race.", + "exampleTranslation": "Ordinal number for one" + }, + { + "word": "Second", + "translation": "Position number 2 in a sequence", + "phonetic": "/ˈsɛkənd/", + "example": "He came in second place.", + "exampleTranslation": "Ordinal number for two" + } + ] +} diff --git a/assets/vocabulary/day3_en.json b/assets/vocabulary/day3_en.json new file mode 100644 index 0000000..88bfe6d --- /dev/null +++ b/assets/vocabulary/day3_en.json @@ -0,0 +1,74 @@ +{ + "vocabulary": [ + { + "word": "Monday", + "translation": "The first day of the work week", + "phonetic": "/ˈmʌndeɪ/", + "example": "I have a meeting on Monday morning.", + "exampleTranslation": "The day after Sunday" + }, + { + "word": "Tuesday", + "translation": "The second day of the work week", + "phonetic": "/ˈtuːzdeɪ/", + "example": "Tuesday is my favorite day.", + "exampleTranslation": "The day between Monday and Wednesday" + }, + { + "word": "Wednesday", + "translation": "The third day of the work week, middle of the week", + "phonetic": "/ˈwɛnzdeɪ/", + "example": "We meet every Wednesday.", + "exampleTranslation": "Often called 'hump day'" + }, + { + "word": "Friday", + "translation": "The last day of the work week", + "phonetic": "/ˈfraɪdeɪ/", + "example": "I'm excited for Friday!", + "exampleTranslation": "The day before the weekend" + }, + { + "word": "Saturday", + "translation": "First day of the weekend", + "phonetic": "/ˈsætərdeɪ/", + "example": "Let's go to the park on Saturday.", + "exampleTranslation": "A day for relaxation and activities" + }, + { + "word": "Sunday", + "translation": "Second day of the weekend, last day of the week", + "phonetic": "/ˈsʌndeɪ/", + "example": "Sunday is a rest day.", + "exampleTranslation": "Often a day for family and rest" + }, + { + "word": "January", + "translation": "First month of the year", + "phonetic": "/ˈdʒænjuˌɛri/", + "example": "January is very cold here.", + "exampleTranslation": "The month after December" + }, + { + "word": "February", + "translation": "Second month of the year, shortest month", + "phonetic": "/ˈfɛbruˌɛri/", + "example": "Valentine's Day is in February.", + "exampleTranslation": "Has 28 or 29 days" + }, + { + "word": "March", + "translation": "Third month of the year, spring begins", + "phonetic": "/mɑːrtʃ/", + "example": "Spring starts in March.", + "exampleTranslation": "The month after February" + }, + { + "word": "December", + "translation": "Last month of the year", + "phonetic": "/dɪˈsɛmbər/", + "example": "December is a holiday month.", + "exampleTranslation": "The 12th and final month" + } + ] +} diff --git a/lib/models/exercise.dart b/lib/models/exercise.dart new file mode 100644 index 0000000..c0dbec2 --- /dev/null +++ b/lib/models/exercise.dart @@ -0,0 +1,309 @@ +enum ExerciseType { + multipleChoice, + fillInBlank, + matching, + translation, + listening, +} + +abstract class Exercise { + final String id; + final ExerciseType type; + final String question; + final String? audioHint; + + Exercise({ + required this.id, + required this.type, + required this.question, + this.audioHint, + }); + + bool checkAnswer(dynamic answer); + dynamic getCorrectAnswer(); + Map toJson(); +} + +class MultipleChoiceExercise extends Exercise { + final List options; + final int correctOptionIndex; + + MultipleChoiceExercise({ + required String id, + required String question, + required this.options, + required this.correctOptionIndex, + String? audioHint, + }) : super( + id: id, + type: ExerciseType.multipleChoice, + question: question, + audioHint: audioHint, + ); + + @override + bool checkAnswer(dynamic answer) { + if (answer is! int) return false; + return answer == correctOptionIndex; + } + + @override + dynamic getCorrectAnswer() => correctOptionIndex; + + @override + Map toJson() { + return { + 'id': id, + 'type': 'multipleChoice', + 'question': question, + 'options': options, + 'correctOptionIndex': correctOptionIndex, + 'audioHint': audioHint, + }; + } + + factory MultipleChoiceExercise.fromJson(Map json) { + return MultipleChoiceExercise( + id: json['id'], + question: json['question'], + options: List.from(json['options']), + correctOptionIndex: json['correctOptionIndex'], + audioHint: json['audioHint'], + ); + } +} + +class FillInBlankExercise extends Exercise { + final String correctAnswer; + final List? acceptableAlternatives; + final bool caseSensitive; + + FillInBlankExercise({ + required String id, + required String question, + required this.correctAnswer, + this.acceptableAlternatives, + this.caseSensitive = false, + String? audioHint, + }) : super( + id: id, + type: ExerciseType.fillInBlank, + question: question, + audioHint: audioHint, + ); + + @override + bool checkAnswer(dynamic answer) { + if (answer is! String) return false; + + final userAnswer = caseSensitive ? answer : answer.toLowerCase(); + final correct = caseSensitive ? correctAnswer : correctAnswer.toLowerCase(); + + if (userAnswer == correct) return true; + + if (acceptableAlternatives != null) { + for (final alternative in acceptableAlternatives!) { + final alt = caseSensitive ? alternative : alternative.toLowerCase(); + if (userAnswer == alt) return true; + } + } + + return false; + } + + @override + dynamic getCorrectAnswer() => correctAnswer; + + @override + Map toJson() { + return { + 'id': id, + 'type': 'fillInBlank', + 'question': question, + 'correctAnswer': correctAnswer, + 'acceptableAlternatives': acceptableAlternatives, + 'caseSensitive': caseSensitive, + 'audioHint': audioHint, + }; + } + + factory FillInBlankExercise.fromJson(Map json) { + return FillInBlankExercise( + id: json['id'], + question: json['question'], + correctAnswer: json['correctAnswer'], + acceptableAlternatives: json['acceptableAlternatives'] != null + ? List.from(json['acceptableAlternatives']) + : null, + caseSensitive: json['caseSensitive'] ?? false, + audioHint: json['audioHint'], + ); + } +} + +class MatchingPair { + final String left; + final String right; + + MatchingPair({ + required this.left, + required this.right, + }); + + Map toJson() { + return { + 'left': left, + 'right': right, + }; + } + + factory MatchingPair.fromJson(Map json) { + return MatchingPair( + left: json['left'], + right: json['right'], + ); + } +} + +class MatchingExercise extends Exercise { + final List pairs; + + MatchingExercise({ + required String id, + required String question, + required this.pairs, + String? audioHint, + }) : super( + id: id, + type: ExerciseType.matching, + question: question, + audioHint: audioHint, + ); + + @override + bool checkAnswer(dynamic answer) { + if (answer is! Map) return false; + + for (final pair in pairs) { + if (answer[pair.left] != pair.right) { + return false; + } + } + + return answer.length == pairs.length; + } + + @override + dynamic getCorrectAnswer() { + return Map.fromEntries( + pairs.map((pair) => MapEntry(pair.left, pair.right)), + ); + } + + @override + Map toJson() { + return { + 'id': id, + 'type': 'matching', + 'question': question, + 'pairs': pairs.map((p) => p.toJson()).toList(), + 'audioHint': audioHint, + }; + } + + factory MatchingExercise.fromJson(Map json) { + return MatchingExercise( + id: json['id'], + question: json['question'], + pairs: (json['pairs'] as List) + .map((p) => MatchingPair.fromJson(p)) + .toList(), + audioHint: json['audioHint'], + ); + } +} + +class TranslationExercise extends Exercise { + final String targetText; + final String correctTranslation; + final List? acceptableAlternatives; + + TranslationExercise({ + required String id, + required String question, + required this.targetText, + required this.correctTranslation, + this.acceptableAlternatives, + String? audioHint, + }) : super( + id: id, + type: ExerciseType.translation, + question: question, + audioHint: audioHint, + ); + + @override + bool checkAnswer(dynamic answer) { + if (answer is! String) return false; + + final userAnswer = answer.trim().toLowerCase(); + final correct = correctTranslation.trim().toLowerCase(); + + if (userAnswer == correct) return true; + + if (acceptableAlternatives != null) { + for (final alternative in acceptableAlternatives!) { + if (userAnswer == alternative.trim().toLowerCase()) { + return true; + } + } + } + + return false; + } + + @override + dynamic getCorrectAnswer() => correctTranslation; + + @override + Map toJson() { + return { + 'id': id, + 'type': 'translation', + 'question': question, + 'targetText': targetText, + 'correctTranslation': correctTranslation, + 'acceptableAlternatives': acceptableAlternatives, + 'audioHint': audioHint, + }; + } + + factory TranslationExercise.fromJson(Map json) { + return TranslationExercise( + id: json['id'], + question: json['question'], + targetText: json['targetText'], + correctTranslation: json['correctTranslation'], + acceptableAlternatives: json['acceptableAlternatives'] != null + ? List.from(json['acceptableAlternatives']) + : null, + audioHint: json['audioHint'], + ); + } +} + +// Factory method to create exercises from JSON +Exercise exerciseFromJson(Map json) { + switch (json['type']) { + case 'multipleChoice': + return MultipleChoiceExercise.fromJson(json); + case 'fillInBlank': + return FillInBlankExercise.fromJson(json); + case 'matching': + return MatchingExercise.fromJson(json); + case 'translation': + return TranslationExercise.fromJson(json); + default: + throw Exception('Unknown exercise type: ${json['type']}'); + } +} diff --git a/lib/models/lesson.dart b/lib/models/lesson.dart index 8e67c79..c941b9d 100644 --- a/lib/models/lesson.dart +++ b/lib/models/lesson.dart @@ -1,5 +1,8 @@ +import 'dart:convert'; import 'package:flutter/services.dart'; import 'language.dart'; +import 'exercise.dart'; +import 'vocabulary.dart'; class Lesson { final int day; @@ -8,6 +11,8 @@ class Lesson { final String description; final String audioPath; final String? textContent; + List? exercises; + List? vocabulary; Lesson({ required this.day, @@ -16,6 +21,8 @@ class Lesson { required this.description, required this.audioPath, this.textContent, + this.exercises, + this.vocabulary, }); String get phase { @@ -54,6 +61,49 @@ class Lesson { } } + String getExercisesFilePath() { + return 'assets/exercises/day${day}_${language.code}.json'; + } + + String getVocabularyFilePath() { + return 'assets/vocabulary/day${day}_${language.code}.json'; + } + + Future loadExercises() async { + try { + final jsonString = await rootBundle.loadString(getExercisesFilePath()); + final jsonData = json.decode(jsonString) as Map; + final exercisesList = jsonData['exercises'] as List; + + exercises = exercisesList + .map((e) => exerciseFromJson(e as Map)) + .toList(); + } catch (e) { + // No exercises available for this lesson + exercises = []; + } + } + + Future loadVocabulary() async { + try { + final jsonString = await rootBundle.loadString(getVocabularyFilePath()); + final jsonData = json.decode(jsonString) as Map; + final vocabularyList = jsonData['vocabulary'] as List; + + vocabulary = vocabularyList + .map((v) => VocabularyItem.fromJson(v as Map)) + .toList(); + } catch (e) { + // No vocabulary available for this lesson + vocabulary = []; + } + } + + int get totalExercises => exercises?.length ?? 0; + int get totalVocabulary => vocabulary?.length ?? 0; + bool get hasExercises => totalExercises > 0; + bool get hasVocabulary => totalVocabulary > 0; + static String _getDescription(int day) { final descriptions = { 1: 'Greetings and Basic Introductions', diff --git a/lib/models/progress.dart b/lib/models/progress.dart index 2a97fd4..c64f90b 100644 --- a/lib/models/progress.dart +++ b/lib/models/progress.dart @@ -3,12 +3,16 @@ import 'language.dart'; class Progress { final Map> completedLessons; final Map currentDay; + // Map of language -> day -> set of completed exercise IDs + final Map>> completedExercises; Progress({ Map>? completedLessons, Map? currentDay, + Map>>? completedExercises, }) : completedLessons = completedLessons ?? {}, - currentDay = currentDay ?? {}; + currentDay = currentDay ?? {}, + completedExercises = completedExercises ?? {}; bool isLessonCompleted(Language language, int day) { return completedLessons[language]?.contains(day) ?? false; @@ -27,13 +31,27 @@ class Progress { return completed / 50.0; } + bool isExerciseCompleted(Language language, int day, String exerciseId) { + return completedExercises[language]?[day]?.contains(exerciseId) ?? false; + } + + int getCompletedExercisesCount(Language language, int day) { + return completedExercises[language]?[day]?.length ?? 0; + } + + Set getCompletedExerciseIds(Language language, int day) { + return completedExercises[language]?[day] ?? {}; + } + Progress copyWith({ Map>? completedLessons, Map? currentDay, + Map>>? completedExercises, }) { return Progress( completedLessons: completedLessons ?? this.completedLessons, currentDay: currentDay ?? this.currentDay, + completedExercises: completedExercises ?? this.completedExercises, ); } @@ -45,12 +63,21 @@ class Progress { 'currentDay': currentDay.map( (key, value) => MapEntry(key.code, value), ), + 'completedExercises': completedExercises.map( + (lang, dayMap) => MapEntry( + lang.code, + dayMap.map( + (day, exerciseIds) => MapEntry(day.toString(), exerciseIds.toList()), + ), + ), + ), }; } factory Progress.fromJson(Map json) { final completedLessonsMap = >{}; final currentDayMap = {}; + final completedExercisesMap = >>{}; if (json['completedLessons'] != null) { (json['completedLessons'] as Map).forEach((key, value) { @@ -66,9 +93,24 @@ class Progress { }); } + if (json['completedExercises'] != null) { + (json['completedExercises'] as Map).forEach((langCode, dayMap) { + final language = Language.fromCode(langCode); + final exercisesByDay = >{}; + + (dayMap as Map).forEach((dayStr, exerciseIds) { + final day = int.parse(dayStr); + exercisesByDay[day] = (exerciseIds as List).cast().toSet(); + }); + + completedExercisesMap[language] = exercisesByDay; + }); + } + return Progress( completedLessons: completedLessonsMap, currentDay: currentDayMap, + completedExercises: completedExercisesMap, ); } } diff --git a/lib/models/vocabulary.dart b/lib/models/vocabulary.dart new file mode 100644 index 0000000..3d4c6c3 --- /dev/null +++ b/lib/models/vocabulary.dart @@ -0,0 +1,47 @@ +class VocabularyItem { + final String word; + final String translation; + final String? phonetic; + final String? example; + final String? exampleTranslation; + final String? audioPath; + final String? imageUrl; + final List? tags; + + VocabularyItem({ + required this.word, + required this.translation, + this.phonetic, + this.example, + this.exampleTranslation, + this.audioPath, + this.imageUrl, + this.tags, + }); + + Map toJson() { + return { + 'word': word, + 'translation': translation, + 'phonetic': phonetic, + 'example': example, + 'exampleTranslation': exampleTranslation, + 'audioPath': audioPath, + 'imageUrl': imageUrl, + 'tags': tags, + }; + } + + factory VocabularyItem.fromJson(Map json) { + return VocabularyItem( + word: json['word'], + translation: json['translation'], + phonetic: json['phonetic'], + example: json['example'], + exampleTranslation: json['exampleTranslation'], + audioPath: json['audioPath'], + imageUrl: json['imageUrl'], + tags: json['tags'] != null ? List.from(json['tags']) : null, + ); + } +} diff --git a/lib/screens/lesson_screen.dart b/lib/screens/lesson_screen.dart index 59da2ce..fd26154 100644 --- a/lib/screens/lesson_screen.dart +++ b/lib/screens/lesson_screen.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:audioplayers/audioplayers.dart'; @@ -5,6 +6,7 @@ import '../models/language.dart'; import '../models/lesson.dart'; import '../services/progress_service.dart'; import '../utils/app_localizations.dart'; +import 'practice_screen.dart'; class LessonScreen extends StatefulWidget { final Language language; @@ -29,52 +31,89 @@ class _LessonScreenState extends State { Duration _position = Duration.zero; String _lessonText = ''; + bool _showTranslations = false; + bool _isLoadingContent = false; + + // Stream subscriptions for audio player + StreamSubscription? _durationSubscription; + StreamSubscription? _positionSubscription; + StreamSubscription? _playerStateSubscription; + StreamSubscription? _playerCompleteSubscription; + @override void initState() { super.initState(); _currentDay = widget.initialDay; _currentLesson = Lesson.create(_currentDay, widget.language); _setupAudioPlayer(); - _loadLessonText(); + _loadLessonContent(); } - Future _loadLessonText() async { + Future _loadLessonContent() async { + if (!mounted) return; + + setState(() { + _isLoadingContent = true; + }); + final text = await _currentLesson.loadTextContent(); + await _currentLesson.loadExercises(); + await _currentLesson.loadVocabulary(); + + if (!mounted) return; + setState(() { _lessonText = text; + _isLoadingContent = false; }); } void _setupAudioPlayer() { - _audioPlayer.onDurationChanged.listen((duration) { - setState(() { - _duration = duration; - }); + _durationSubscription = _audioPlayer.onDurationChanged.listen((duration) { + if (mounted) { + setState(() { + _duration = duration; + }); + } }); - _audioPlayer.onPositionChanged.listen((position) { - setState(() { - _position = position; - }); + _positionSubscription = _audioPlayer.onPositionChanged.listen((position) { + if (mounted) { + setState(() { + _position = position; + }); + } }); - _audioPlayer.onPlayerStateChanged.listen((state) { - setState(() { - _isPlaying = state == PlayerState.playing; - }); + _playerStateSubscription = _audioPlayer.onPlayerStateChanged.listen((state) { + if (mounted) { + setState(() { + _isPlaying = state == PlayerState.playing; + }); + } }); - _audioPlayer.onPlayerComplete.listen((_) { - setState(() { - _isPlaying = false; - _position = Duration.zero; - }); + _playerCompleteSubscription = _audioPlayer.onPlayerComplete.listen((_) { + if (mounted) { + setState(() { + _isPlaying = false; + _position = Duration.zero; + }); + } }); } @override void dispose() { + // Cancel all stream subscriptions before disposing the audio player + _durationSubscription?.cancel(); + _positionSubscription?.cancel(); + _playerStateSubscription?.cancel(); + _playerCompleteSubscription?.cancel(); + + // Dispose the audio player _audioPlayer.dispose(); + super.dispose(); } @@ -94,10 +133,11 @@ class _LessonScreenState extends State { _currentLesson = Lesson.create(_currentDay, widget.language); _position = Duration.zero; _isPlaying = false; + _showTranslations = false; }); _audioPlayer.stop(); - _loadLessonText(); + _loadLessonContent(); } String _formatDuration(Duration duration) { @@ -295,6 +335,222 @@ class _LessonScreenState extends State { const SizedBox(height: 16), + // Practice Section + if (_currentLesson.hasExercises) + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Icon( + Icons.quiz, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + 'Interactive Practice', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + Consumer( + builder: (context, progressService, child) { + final completedCount = progressService.getCompletedExercisesCount( + widget.language, + _currentDay, + ); + final totalCount = _currentLesson.totalExercises; + final progress = totalCount > 0 ? completedCount / totalCount : 0.0; + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Progress:', + style: Theme.of(context).textTheme.titleMedium, + ), + Text( + '$completedCount / $totalCount exercises', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 8), + LinearProgressIndicator( + value: progress, + minHeight: 8, + borderRadius: BorderRadius.circular(4), + ), + const SizedBox(height: 16), + ElevatedButton.icon( + onPressed: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PracticeScreen( + lesson: _currentLesson, + language: widget.language, + ), + ), + ); + }, + icon: const Icon(Icons.play_arrow), + label: Text( + completedCount == 0 + ? 'Start Practice' + : completedCount == totalCount + ? 'Review Exercises' + : 'Continue Practice', + ), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(16), + backgroundColor: Theme.of(context).colorScheme.secondary, + foregroundColor: Colors.white, + ), + ), + ], + ); + }, + ), + ], + ), + ), + ), + + if (_currentLesson.hasExercises) const SizedBox(height: 16), + + // Vocabulary Section + if (_currentLesson.hasVocabulary) + Card( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + children: [ + Icon( + Icons.book, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(width: 8), + Text( + 'Vocabulary', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ], + ), + TextButton.icon( + onPressed: () { + setState(() { + _showTranslations = !_showTranslations; + }); + }, + icon: Icon(_showTranslations ? Icons.visibility_off : Icons.visibility), + label: Text(_showTranslations ? 'Hide' : 'Show'), + ), + ], + ), + const SizedBox(height: 16), + ..._currentLesson.vocabulary!.map((vocab) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + vocab.word, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + ), + if (vocab.phonetic != null) + Text( + vocab.phonetic!, + style: TextStyle( + color: Colors.grey.shade600, + fontStyle: FontStyle.italic, + ), + ), + ], + ), + if (_showTranslations) ...[ + const SizedBox(height: 8), + Text( + vocab.translation, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + if (vocab.example != null && _showTranslations) ...[ + const SizedBox(height: 8), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(4), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + vocab.example!, + style: const TextStyle(fontStyle: FontStyle.italic), + ), + if (vocab.exampleTranslation != null) ...[ + const SizedBox(height: 4), + Text( + vocab.exampleTranslation!, + style: TextStyle( + fontSize: 12, + color: Colors.grey.shade600, + ), + ), + ], + ], + ), + ), + ], + ], + ), + ), + ); + }), + ], + ), + ), + ), + + if (_currentLesson.hasVocabulary) const SizedBox(height: 16), + // Mark Complete Button ElevatedButton.icon( onPressed: () async { diff --git a/lib/screens/practice_screen.dart b/lib/screens/practice_screen.dart new file mode 100644 index 0000000..58a02cb --- /dev/null +++ b/lib/screens/practice_screen.dart @@ -0,0 +1,579 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import '../models/language.dart'; +import '../models/lesson.dart'; +import '../models/exercise.dart'; +import '../services/progress_service.dart'; +import '../utils/app_localizations.dart'; + +class PracticeScreen extends StatefulWidget { + final Lesson lesson; + final Language language; + + const PracticeScreen({ + super.key, + required this.lesson, + required this.language, + }); + + @override + State createState() => _PracticeScreenState(); +} + +class _PracticeScreenState extends State { + int _currentExerciseIndex = 0; + Map _userAnswers = {}; + Map _exerciseResults = {}; + bool _showFeedback = false; + bool _allExercisesCompleted = false; + + @override + void initState() { + super.initState(); + // Initialize with already completed exercises + final progressService = Provider.of(context, listen: false); + final completedIds = progressService.getCompletedExerciseIds( + widget.language, + widget.lesson.day, + ); + + for (final exerciseId in completedIds) { + _exerciseResults[exerciseId] = true; + } + } + + Exercise get _currentExercise => widget.lesson.exercises![_currentExerciseIndex]; + + bool get _canProceed => _exerciseResults[_currentExercise.id] == true; + + int get _completedCount => _exerciseResults.values.where((v) => v == true).length; + + int get _totalExercises => widget.lesson.exercises?.length ?? 0; + + void _submitAnswer() { + final answer = _userAnswers[_currentExercise.id]; + if (answer == null) return; + + final isCorrect = _currentExercise.checkAnswer(answer); + + setState(() { + _exerciseResults[_currentExercise.id] = isCorrect; + _showFeedback = true; + }); + + if (isCorrect) { + final progressService = Provider.of(context, listen: false); + progressService.markExerciseComplete( + widget.language, + widget.lesson.day, + _currentExercise.id, + ); + } + } + + void _nextExercise() { + if (_currentExerciseIndex < _totalExercises - 1) { + setState(() { + _currentExerciseIndex++; + _showFeedback = false; + }); + } else { + setState(() { + _allExercisesCompleted = true; + }); + } + } + + void _previousExercise() { + if (_currentExerciseIndex > 0) { + setState(() { + _currentExerciseIndex--; + _showFeedback = false; + }); + } + } + + Widget _buildExerciseWidget() { + final exercise = _currentExercise; + + if (exercise is MultipleChoiceExercise) { + return _buildMultipleChoice(exercise); + } else if (exercise is FillInBlankExercise) { + return _buildFillInBlank(exercise); + } else if (exercise is MatchingExercise) { + return _buildMatching(exercise); + } else if (exercise is TranslationExercise) { + return _buildTranslation(exercise); + } + + return const Text('Unknown exercise type'); + } + + Widget _buildMultipleChoice(MultipleChoiceExercise exercise) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + exercise.question, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 24), + ...List.generate(exercise.options.length, (index) { + final isSelected = _userAnswers[exercise.id] == index; + final showResult = _showFeedback && isSelected; + final isCorrect = index == exercise.correctOptionIndex; + + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: OutlinedButton( + onPressed: _showFeedback + ? null + : () { + setState(() { + _userAnswers[exercise.id] = index; + }); + }, + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.all(16), + backgroundColor: showResult + ? (_exerciseResults[exercise.id] == true + ? Colors.green.withValues(alpha: 0.1) + : Colors.red.withValues(alpha: 0.1)) + : (isSelected + ? Theme.of(context).colorScheme.primaryContainer + : null), + side: BorderSide( + color: showResult + ? (_exerciseResults[exercise.id] == true + ? Colors.green + : Colors.red) + : (isSelected + ? Theme.of(context).colorScheme.primary + : Colors.grey), + width: 2, + ), + ), + child: Row( + children: [ + Expanded( + child: Text( + exercise.options[index], + style: TextStyle( + color: showResult + ? (_exerciseResults[exercise.id] == true + ? Colors.green.shade700 + : Colors.red.shade700) + : null, + ), + ), + ), + if (showResult) + Icon( + _exerciseResults[exercise.id] == true + ? Icons.check_circle + : Icons.cancel, + color: _exerciseResults[exercise.id] == true + ? Colors.green + : Colors.red, + ), + if (_showFeedback && isCorrect && !isSelected) + const Icon(Icons.check_circle, color: Colors.green), + ], + ), + ), + ); + }), + ], + ); + } + + Widget _buildFillInBlank(FillInBlankExercise exercise) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + exercise.question, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 24), + TextField( + enabled: !_showFeedback, + onChanged: (value) { + setState(() { + _userAnswers[exercise.id] = value; + }); + }, + decoration: InputDecoration( + hintText: 'Type your answer here...', + border: const OutlineInputBorder(), + suffixIcon: _showFeedback + ? Icon( + _exerciseResults[exercise.id] == true + ? Icons.check_circle + : Icons.cancel, + color: _exerciseResults[exercise.id] == true + ? Colors.green + : Colors.red, + ) + : null, + ), + ), + if (_showFeedback && _exerciseResults[exercise.id] == false) + Padding( + padding: const EdgeInsets.only(top: 12), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green), + ), + child: Text( + 'Correct answer: ${exercise.correctAnswer}', + style: TextStyle( + color: Colors.green.shade700, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ); + } + + Widget _buildMatching(MatchingExercise exercise) { + final leftItems = exercise.pairs.map((p) => p.left).toList(); + final rightItems = exercise.pairs.map((p) => p.right).toList()..shuffle(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + exercise.question, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 24), + const Text('Match the items (tap to select, then tap the matching item):'), + const SizedBox(height: 16), + // For simplicity, showing a message - full implementation would need drag-and-drop + const Text( + 'Matching exercise UI - Tap items to connect them', + style: TextStyle(fontStyle: FontStyle.italic), + ), + // Simplified version: display pairs + ...exercise.pairs.map((pair) { + return Card( + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + Expanded(child: Text(pair.left)), + const Icon(Icons.arrow_forward), + Expanded(child: Text(pair.right)), + ], + ), + ), + ); + }), + ], + ); + } + + Widget _buildTranslation(TranslationExercise exercise) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + exercise.question, + style: Theme.of(context).textTheme.headlineSmall, + ), + const SizedBox(height: 24), + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Text( + exercise.targetText, + style: Theme.of(context).textTheme.titleLarge, + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 24), + TextField( + enabled: !_showFeedback, + onChanged: (value) { + setState(() { + _userAnswers[exercise.id] = value; + }); + }, + decoration: InputDecoration( + hintText: 'Type your translation...', + border: const OutlineInputBorder(), + suffixIcon: _showFeedback + ? Icon( + _exerciseResults[exercise.id] == true + ? Icons.check_circle + : Icons.cancel, + color: _exerciseResults[exercise.id] == true + ? Colors.green + : Colors.red, + ) + : null, + ), + maxLines: 3, + ), + if (_showFeedback && _exerciseResults[exercise.id] == false) + Padding( + padding: const EdgeInsets.only(top: 12), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.green.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.green), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Correct translation:', + style: TextStyle( + color: Colors.green.shade700, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + exercise.correctTranslation, + style: TextStyle(color: Colors.green.shade700), + ), + ], + ), + ), + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + final loc = AppLocalizations.of(context); + + if (_allExercisesCompleted) { + return Scaffold( + appBar: AppBar( + title: Text('Practice - ${loc.translate('lesson.day')} ${widget.lesson.day}'), + ), + body: Center( + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.celebration, + size: 100, + color: Theme.of(context).colorScheme.primary, + ), + const SizedBox(height: 24), + Text( + 'Congratulations!', + style: Theme.of(context).textTheme.headlineMedium, + ), + const SizedBox(height: 16), + Text( + 'You\'ve completed all exercises for this lesson!', + style: Theme.of(context).textTheme.titleMedium, + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Text( + '$_completedCount / $_totalExercises exercises completed', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: Theme.of(context).colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 32), + ElevatedButton.icon( + onPressed: () => Navigator.pop(context), + icon: const Icon(Icons.arrow_back), + label: const Text('Back to Lesson'), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + ), + ), + ], + ), + ), + ), + ); + } + + return Scaffold( + appBar: AppBar( + title: Text('Practice - ${loc.translate('lesson.day')} ${widget.lesson.day}'), + actions: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Chip( + avatar: Text( + widget.language.flag, + style: const TextStyle(fontSize: 16), + ), + label: Text( + '$_completedCount / $_totalExercises', + style: const TextStyle(fontWeight: FontWeight.bold), + ), + ), + ), + ], + ), + body: Column( + children: [ + // Progress indicator + LinearProgressIndicator( + value: _totalExercises > 0 ? _completedCount / _totalExercises : 0, + minHeight: 8, + ), + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Exercise counter + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Exercise ${_currentExerciseIndex + 1} of $_totalExercises', + style: Theme.of(context).textTheme.titleMedium, + ), + if (_exerciseResults[_currentExercise.id] == true) + const Chip( + avatar: Icon(Icons.check, size: 16), + label: Text('Completed'), + backgroundColor: Colors.green, + labelStyle: TextStyle(color: Colors.white), + ), + ], + ), + const SizedBox(height: 24), + + // Exercise content + _buildExerciseWidget(), + + const SizedBox(height: 32), + + // Feedback message + if (_showFeedback) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: _exerciseResults[_currentExercise.id] == true + ? Colors.green.withValues(alpha: 0.1) + : Colors.red.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: _exerciseResults[_currentExercise.id] == true + ? Colors.green + : Colors.red, + width: 2, + ), + ), + child: Row( + children: [ + Icon( + _exerciseResults[_currentExercise.id] == true + ? Icons.check_circle + : Icons.cancel, + color: _exerciseResults[_currentExercise.id] == true + ? Colors.green + : Colors.red, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + _exerciseResults[_currentExercise.id] == true + ? 'Excellent! That\'s correct!' + : 'Not quite right. Try reviewing the lesson content.', + style: TextStyle( + color: _exerciseResults[_currentExercise.id] == true + ? Colors.green.shade700 + : Colors.red.shade700, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + + const SizedBox(height: 24), + + // Action buttons + if (!_showFeedback) + ElevatedButton( + onPressed: _userAnswers.containsKey(_currentExercise.id) + ? _submitAnswer + : null, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(16), + ), + child: const Text('Submit Answer'), + ), + + if (_showFeedback) + ElevatedButton( + onPressed: _canProceed ? _nextExercise : null, + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.all(16), + ), + child: Text( + _currentExerciseIndex < _totalExercises - 1 + ? 'Next Exercise' + : 'Finish Practice', + ), + ), + + const SizedBox(height: 16), + + // Navigation buttons + Row( + children: [ + Expanded( + child: OutlinedButton.icon( + onPressed: _currentExerciseIndex > 0 ? _previousExercise : null, + icon: const Icon(Icons.arrow_back), + label: const Text('Previous'), + ), + ), + const SizedBox(width: 16), + Expanded( + child: OutlinedButton.icon( + onPressed: _currentExerciseIndex < _totalExercises - 1 + ? _nextExercise + : null, + icon: const Icon(Icons.arrow_forward), + label: const Text('Skip'), + ), + ), + ], + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/services/progress_service.dart b/lib/services/progress_service.dart index 9c48f54..448a686 100644 --- a/lib/services/progress_service.dart +++ b/lib/services/progress_service.dart @@ -48,6 +48,7 @@ class ProgressService extends ChangeNotifier { _progress = Progress( completedLessons: newCompletedLessons, currentDay: newCurrentDay, + completedExercises: _progress.completedExercises, ); await _saveProgress(); @@ -64,6 +65,7 @@ class ProgressService extends ChangeNotifier { _progress = Progress( completedLessons: newCompletedLessons, currentDay: _progress.currentDay, + completedExercises: _progress.completedExercises, ); await _saveProgress(); @@ -85,4 +87,41 @@ class ProgressService extends ChangeNotifier { double getProgress(Language language) { return _progress.getProgress(language); } + + Future markExerciseComplete(Language language, int day, String exerciseId) async { + final newCompletedExercises = Map>>.from(_progress.completedExercises); + + if (!newCompletedExercises.containsKey(language)) { + newCompletedExercises[language] = {}; + } + + if (!newCompletedExercises[language]!.containsKey(day)) { + newCompletedExercises[language]![day] = {}; + } + + final dayExercises = Set.from(newCompletedExercises[language]![day]!); + dayExercises.add(exerciseId); + newCompletedExercises[language]![day] = dayExercises; + + _progress = Progress( + completedLessons: _progress.completedLessons, + currentDay: _progress.currentDay, + completedExercises: newCompletedExercises, + ); + + await _saveProgress(); + notifyListeners(); + } + + bool isExerciseCompleted(Language language, int day, String exerciseId) { + return _progress.isExerciseCompleted(language, day, exerciseId); + } + + int getCompletedExercisesCount(Language language, int day) { + return _progress.getCompletedExercisesCount(language, day); + } + + Set getCompletedExerciseIds(Language language, int day) { + return _progress.getCompletedExerciseIds(language, day); + } } diff --git a/pubspec.yaml b/pubspec.yaml index 9492693..0c6b229 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -44,6 +44,8 @@ flutter: - assets/audio/ - assets/translations/ - assets/lessons/ + - assets/exercises/ + - assets/vocabulary/ # fonts: # - family: Poppins