Skip to content

Commit 323fb0d

Browse files
committed
feat(flashcard): updated widget usability across different pages
feat(dictionary): updated dictionary system to use `firebase_ui_firestore`
1 parent 8a6737b commit 323fb0d

File tree

8 files changed

+103
-157
lines changed

8 files changed

+103
-157
lines changed

src/lib/common/utils/data_provider.dart

Lines changed: 0 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import 'package:firebase_auth/firebase_auth.dart';
33
import 'package:flutter/material.dart';
44
import 'package:cloud_firestore/cloud_firestore.dart';
55
import 'package:spaced_repetition/sm.dart';
6-
import '../../features/dictionary/dictionary_model.dart';
76
import '../../features/flashcard/flashcard_model.dart';
87
import '../../features/review/review_model.dart';
98

@@ -12,12 +11,8 @@ const pageSize = 5;
1211

1312
class DataProvider extends ChangeNotifier {
1413
Map<String, List<ReviewModel>> _reviews = {};
15-
List<DictionaryModel> _dictionary = [];
16-
17-
late DocumentSnapshot<Map<String, dynamic>> _lastCardDoc;
1814

1915
Map<String, List<ReviewModel>> get reviews => _reviews;
20-
List<DictionaryModel> get dictionary => _dictionary;
2116

2217
Future<void> loadReviews() async {
2318
try {
@@ -121,43 +116,4 @@ class DataProvider extends ChangeNotifier {
121116
_reviews.remove(deckTitle);
122117
notifyListeners();
123118
}
124-
125-
Future<void> loadDictionary() async {
126-
final dictionary = await FirebaseFirestore.instance
127-
.collectionGroup('cards')
128-
.where('type', isEqualTo: 'immutable')
129-
.limit(pageSize)
130-
.get();
131-
132-
_dictionary = dictionary.docs
133-
.map((doc) => DictionaryModel.fromMap(doc.id, doc.data()))
134-
.toList();
135-
136-
_lastCardDoc = dictionary.docs.last;
137-
138-
notifyListeners();
139-
}
140-
141-
Future<void> loadMoreDictionary() async {
142-
final cards = await FirebaseFirestore.instance
143-
.collectionGroup('cards')
144-
.where('type', isEqualTo: 'immutable')
145-
.startAfterDocument(_lastCardDoc)
146-
.limit(pageSize)
147-
.get();
148-
149-
// If there are no more cards to fetch, return the current list of dictionary
150-
if (cards.docs.isEmpty) {
151-
return;
152-
}
153-
154-
_dictionary = _dictionary
155-
..addAll(cards.docs
156-
.map((doc) => DictionaryModel.fromMap(doc.id, doc.data()))
157-
.toList());
158-
159-
_lastCardDoc = cards.docs.last;
160-
161-
notifyListeners();
162-
}
163119
}

src/lib/features/dictionary/dictionary_model.dart

Lines changed: 0 additions & 24 deletions
This file was deleted.
Lines changed: 42 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
import 'package:asl/features/flashcard/flashcard_model.dart';
2+
import 'package:cloud_firestore/cloud_firestore.dart';
3+
import 'package:firebase_ui_firestore/firebase_ui_firestore.dart';
14
import 'package:flutter/material.dart';
2-
import 'package:provider/provider.dart';
3-
import '../../common/utils/data_provider.dart';
5+
import '../flashcard/flashcard.dart';
46

57
class DictionaryPage extends StatefulWidget {
68
const DictionaryPage({super.key});
@@ -10,50 +12,49 @@ class DictionaryPage extends StatefulWidget {
1012
}
1113

1214
class _DictionaryPageState extends State<DictionaryPage> {
13-
final ScrollController _scrollController = ScrollController();
14-
15-
@override
16-
void initState() {
17-
super.initState();
18-
_scrollController.addListener(_scrollListener);
19-
}
20-
2115
@override
2216
Widget build(BuildContext context) {
23-
final cards = context.watch<DataProvider>().dictionary;
17+
final flashcardsQuery = FirebaseFirestore.instance
18+
.collectionGroup('cards')
19+
.where('type', isEqualTo: 'immutable')
20+
.orderBy('title')
21+
.withConverter<FlashcardModel>(
22+
fromFirestore: (snapshot, _) =>
23+
FlashcardModel.fromMap(snapshot.data()!),
24+
toFirestore: (flashcard, _) => flashcard.toMap(),
25+
);
2426

2527
// TODO add search bar
26-
// TODO restyle with better loading indicator and better card design
27-
return Scrollbar(
28-
thumbVisibility: true,
29-
controller: _scrollController,
30-
child: ListView.builder(
31-
controller: _scrollController,
32-
itemCount: cards.length + 1,
33-
itemBuilder: (context, index) {
34-
if (index == cards.length) {
35-
return SizedBox(
36-
height: MediaQuery.of(context).size.height,
37-
child: const Center(child: CircularProgressIndicator()),
38-
);
39-
}
4028

41-
return Card(
42-
child: ListTile(
43-
title: Text(cards[index].title),
44-
subtitle: Text(cards[index].instructions),
45-
leading: Image.network(cards[index].image),
46-
),
47-
);
48-
}),
29+
return FirestoreQueryBuilder<FlashcardModel>(
30+
query: flashcardsQuery,
31+
builder: (context, snapshot, _) {
32+
if (snapshot.isFetching) {
33+
return const CircularProgressIndicator();
34+
}
35+
if (snapshot.hasError) {
36+
debugPrint('error ${snapshot.error}');
37+
return Text('error ${snapshot.error}');
38+
}
39+
40+
return Wrap(
41+
spacing: 8,
42+
runSpacing: 8,
43+
children: List.generate(
44+
snapshot.docs.length,
45+
(index) {
46+
if (snapshot.hasMore && index + 1 == snapshot.docs.length) {
47+
snapshot.fetchMore();
48+
}
49+
50+
return Flashcard(
51+
card: snapshot.docs[index].data(),
52+
type: CardType.dictionary,
53+
);
54+
},
55+
),
56+
);
57+
},
4958
);
5059
}
51-
52-
void _scrollListener() {
53-
if (_scrollController.offset >=
54-
_scrollController.position.maxScrollExtent &&
55-
!_scrollController.position.outOfRange) {
56-
context.read<DataProvider>().loadMoreDictionary();
57-
}
58-
}
5960
}

src/lib/features/flashcard/flashcard.dart

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@ import 'flashcard_model.dart';
55
import '../../common/utils/data_provider.dart';
66
import '../../common/widgets/custom_iconbutton.dart';
77

8+
enum CardType { review, dictionary, lesson }
9+
810
class Flashcard extends StatefulWidget {
911
const Flashcard({
1012
super.key,
11-
required this.handleIndex,
13+
this.handleIndex,
1214
required this.card,
13-
required this.isReview,
15+
required this.type,
1416
});
1517

16-
final void Function() handleIndex;
18+
final void Function()? handleIndex;
1719
final FlashcardModel card;
18-
final bool isReview;
20+
final CardType type;
1921

2022
@override
2123
State<Flashcard> createState() => _FlashcardState();
@@ -43,13 +45,14 @@ class _FlashcardState extends State<Flashcard> {
4345
Stack(
4446
children: [
4547
_buildImage(),
46-
if (!_isImageBlurred && !isEmptyInstructions)
48+
if ((!_isImageBlurred || widget.type == CardType.dictionary) &&
49+
!isEmptyInstructions)
4750
_buildInstructionsPopup(context),
4851
],
4952
),
5053
// TODO media controls implementation
5154
_buildMediaControls(),
52-
_buildFlashcardButtons(),
55+
if (widget.type != CardType.dictionary) _buildFlashcardButtons(),
5356
],
5457
),
5558
),
@@ -67,13 +70,6 @@ class _FlashcardState extends State<Flashcard> {
6770
),
6871
);
6972

70-
Widget _buildDifficultyButton(String text, int quality, Color color) =>
71-
TextButton(
72-
style: TextButton.styleFrom(foregroundColor: color),
73-
onPressed: () => _handleButtonPress(widget.card, quality),
74-
child: Text(text),
75-
);
76-
7773
Widget _buildMediaControls() => Row(
7874
mainAxisSize: MainAxisSize.min,
7975
children: [
@@ -106,17 +102,15 @@ class _FlashcardState extends State<Flashcard> {
106102
);
107103

108104
Widget _buildImage() => ClipRRect(
109-
child: ImageFiltered(
110-
enabled: _isImageBlurred,
111-
imageFilter: ImageFilter.blur(sigmaX: 48, sigmaY: 48),
112-
child: Image.network(widget.card.image)),
105+
child: widget.type == CardType.dictionary
106+
? Image.network(widget.card.image)
107+
: ImageFiltered(
108+
enabled: _isImageBlurred,
109+
imageFilter: ImageFilter.blur(sigmaX: 48, sigmaY: 48),
110+
child: Image.network(widget.card.image),
111+
),
113112
);
114113

115-
void _handleButtonPress(FlashcardModel flashcard, int quality) {
116-
context.read<DataProvider>().updateCardProgress(flashcard, quality);
117-
widget.handleIndex();
118-
}
119-
120114
Widget _buildFlashcardButtons() {
121115
if (_isImageBlurred) {
122116
return TextButton(
@@ -125,7 +119,7 @@ class _FlashcardState extends State<Flashcard> {
125119
);
126120
}
127121

128-
if (widget.isReview) {
122+
if (widget.type == CardType.review) {
129123
return Row(
130124
mainAxisSize: MainAxisSize.min,
131125
children: [
@@ -142,6 +136,18 @@ class _FlashcardState extends State<Flashcard> {
142136
);
143137
}
144138

139+
Widget _buildDifficultyButton(String text, int quality, Color color) =>
140+
TextButton(
141+
style: TextButton.styleFrom(foregroundColor: color),
142+
onPressed: () => _handleButtonPress(widget.card, quality),
143+
child: Text(text),
144+
);
145+
146+
void _handleButtonPress(FlashcardModel flashcard, int quality) {
147+
context.read<DataProvider>().updateCardProgress(flashcard, quality);
148+
widget.handleIndex!();
149+
}
150+
145151
Widget _buildInstructions(height, width) => Card(
146152
child: Padding(
147153
padding: const EdgeInsets.all(8.0),
Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
class FlashcardModel {
2-
String deckTitle;
3-
String cardId;
4-
String deckId;
2+
String? deckTitle;
3+
String? cardId;
4+
String? deckId;
55
String title;
66
String instructions;
77
String image;
88

9-
FlashcardModel(
10-
{required this.deckTitle,
11-
required this.cardId,
12-
required this.deckId,
13-
required this.title,
14-
required this.instructions,
15-
required this.image});
9+
FlashcardModel({
10+
this.deckTitle,
11+
this.cardId,
12+
this.deckId,
13+
required this.title,
14+
required this.instructions,
15+
required this.image,
16+
});
1617

1718
factory FlashcardModel.fromMap(
18-
Map<String, dynamic> data,
19-
String cardId,
20-
String deckId,
21-
String deckTitle,
22-
) {
19+
Map<String, dynamic> data, [
20+
String? cardId,
21+
String? deckId,
22+
String? deckTitle,
23+
]) {
2324
return FlashcardModel(
2425
deckTitle: deckTitle,
2526
cardId: cardId,
@@ -29,4 +30,12 @@ class FlashcardModel {
2930
image: data['image'],
3031
);
3132
}
33+
34+
Map<String, dynamic> toMap() {
35+
return {
36+
'title': title,
37+
'instructions': instructions,
38+
'image': image,
39+
};
40+
}
3241
}

src/lib/features/home/home_page.dart

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
import 'package:flutter/material.dart';
2-
import 'package:provider/provider.dart';
3-
import '../../common/utils/data_provider.dart';
42
import '../../common/widgets/custom_tab.dart';
53
import '../creator/creator_page.dart';
64
import '../dictionary/dictionary_page.dart';
@@ -19,14 +17,10 @@ class _HomePageState extends State<HomePage> {
1917
@override
2018
void initState() {
2119
super.initState();
22-
23-
context.read<DataProvider>().loadDictionary();
2420
}
2521

2622
@override
2723
Widget build(BuildContext context) {
28-
// TODO restyle Tabs to have proper on hover and on select icon colours
29-
3024
return SafeArea(
3125
child: DefaultTabController(
3226
length: 5,

src/lib/features/lesson/lesson_details_page.dart

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,14 @@ class _LessonDetailsState extends State<LessonDetails> {
6262

6363
List<Widget> _getFlashcards(List<QueryDocumentSnapshot> cards) => cards
6464
.map((card) => Flashcard(
65-
card: FlashcardModel.fromMap(card.data() as Map<String, dynamic>,
66-
card.id, widget.lessonId, widget.lessonData.title),
65+
card: FlashcardModel.fromMap(
66+
card.data() as Map<String, dynamic>,
67+
card.id,
68+
widget.lessonId,
69+
widget.lessonData.title,
70+
),
6771
handleIndex: _handleIndex,
68-
isReview: false,
72+
type: CardType.lesson,
6973
))
7074
.toList();
7175

0 commit comments

Comments
 (0)