Skip to content

Commit

Permalink
Merge pull request #18 from michaelroudnitski/editable-suggestions
Browse files Browse the repository at this point in the history
Editable suggestions - closes #7
  • Loading branch information
michaelroudnitski authored Jan 28, 2020
2 parents 00b8206 + 7e122f9 commit 97df53d
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 71 deletions.
8 changes: 7 additions & 1 deletion lib/controllers/suggestionsController.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,26 @@ import 'package:firebase_auth/firebase_auth.dart';
import 'package:tabs/services/auth.dart';

abstract class SuggestionsController {
static Map<String, dynamic> _defaultSuggestions = {
"names": [],
"amounts": ["5", "10", "20", "50"],
"descriptions": {"Food": 0, "Rent": 0, "A job well done": 0}
};

static Future<Map<String, dynamic>> fetchSuggestions() async {
try {
FirebaseUser user = await Auth.getCurrentUser();
DocumentSnapshot doc = await Firestore.instance
.collection("suggestions")
.document(user.uid)
.get();
if (doc == null) return _defaultSuggestions;
return {
"names": List<String>.from(doc.data["names"]),
"amounts": List<String>.from(doc.data["amounts"]),
"descriptions": Map<String, int>.from(doc.data["descriptions"])
};
} catch (e) {
/* don't need to do anything (we always have default suggestions) */
return null;
}
}
Expand Down
147 changes: 98 additions & 49 deletions lib/create.form.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import 'package:tabs/services/contacts.dart';
import 'package:flutter_typeahead/flutter_typeahead.dart';
import 'package:tabs/providers/settingsState.dart';

// TODO: refactor this monstrosity of a class

class CreateForm extends StatefulWidget {
@override
_CreateFormState createState() => _CreateFormState();
Expand All @@ -24,6 +26,7 @@ class _CreateFormState extends State<CreateForm> {
List<Widget> _pages;
double _formProgress = 0.15;
bool userOwesFriend = false;
bool suggestionsRemovable = false;

@override
void initState() {
Expand All @@ -47,6 +50,7 @@ class _CreateFormState extends State<CreateForm> {
duration: const Duration(milliseconds: 300),
curve: Curves.fastOutSlowIn,
);
suggestionsRemovable = false;
FocusScope.of(context).unfocus();
}

Expand All @@ -56,6 +60,7 @@ class _CreateFormState extends State<CreateForm> {
duration: const Duration(milliseconds: 300),
curve: Curves.fastOutSlowIn,
);
suggestionsRemovable = false;
FocusScope.of(context).unfocus();
}

Expand All @@ -65,6 +70,7 @@ class _CreateFormState extends State<CreateForm> {
@required Widget textField,
@required int pageIndex,
Widget option,
Suggestions suggestionsState,
List<String> suggestions,
}) {
List<Widget> generateSuggestions() {
Expand All @@ -73,19 +79,37 @@ class _CreateFormState extends State<CreateForm> {
for (String suggestion in suggestions) {
chips.add(Padding(
padding: const EdgeInsets.only(right: 8.0),
child: ActionChip(
label: Text(suggestion),
backgroundColor: Colors.white,
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
onPressed: () {
_textControllers[pageIndex].text = suggestion;
},
),
child: suggestionsRemovable
? Chip(
label: Text(suggestion),
backgroundColor: Colors.white,
deleteIconColor: Colors.red,
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
onDeleted: () {
if (pageIndex == 0)
suggestionsState.removeName(suggestion);
else if (pageIndex == 2)
suggestionsState.removeDescription(suggestion);
},
)
: ActionChip(
label: Text(suggestion),
backgroundColor: Colors.white,
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(8),
),
),
onPressed: () {
_textControllers[pageIndex].text = suggestion;
},
),
));
}
return chips;
Expand All @@ -102,22 +126,44 @@ class _CreateFormState extends State<CreateForm> {
style: Theme.of(context).textTheme.headline4,
),
Text(description),
AnimationLimiter(
child: Row(
children: AnimationConfiguration.toStaggeredList(
delay: Duration(milliseconds: 150),
duration: Duration(milliseconds: 200),
childAnimationBuilder: (widget) => SlideAnimation(
horizontalOffset: 70.0,
child: FadeInAnimation(
duration: Duration(milliseconds: 300),
child: widget,
if (suggestions != null && suggestions.length > 0)
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Flexible(
child: AnimationLimiter(
child: Container(
height: 50,
child: ListView(
scrollDirection: Axis.horizontal,
children: AnimationConfiguration.toStaggeredList(
delay: Duration(milliseconds: 150),
duration: Duration(milliseconds: 200),
childAnimationBuilder: (widget) => SlideAnimation(
horizontalOffset: 70.0,
child: FadeInAnimation(
duration: Duration(milliseconds: 300),
child: widget,
),
),
children: generateSuggestions(),
),
),
),
),
),
children: generateSuggestions(),
),
if (pageIndex != 1)
IconButton(
color: Theme.of(context).primaryColor,
icon: Icon(
suggestionsRemovable ? Icons.done_all : Icons.edit),
onPressed: () {
setState(() {
suggestionsRemovable = !suggestionsRemovable;
});
}),
],
),
),
suggestions != null && suggestions.length > 0
? SizedBox(height: 12)
: SizedBox(height: 42),
Expand Down Expand Up @@ -179,7 +225,8 @@ class _CreateFormState extends State<CreateForm> {
buildPage(
pageIndex: 0,
title: "Name",
description: "Enter the name of the person who owes you money.",
description:
"Enter the name of the person who you're making this tab for.",
textField: TypeAheadFormField(
textFieldConfiguration: TextFieldConfiguration(
controller: _textControllers[0],
Expand Down Expand Up @@ -208,6 +255,7 @@ class _CreateFormState extends State<CreateForm> {
},
),
suggestions: suggestionsState.suggestions["names"],
suggestionsState: suggestionsState,
),
buildPage(
pageIndex: 1,
Expand All @@ -221,8 +269,6 @@ class _CreateFormState extends State<CreateForm> {
visualDensity: VisualDensity.comfortable,
enableFeedback: true,
tooltip: "Tap to change who owes who",
highlightColor: Colors.transparent,
splashColor: Colors.transparent,
onPressed: () {
setState(() {
userOwesFriend = !userOwesFriend;
Expand All @@ -243,28 +289,31 @@ class _CreateFormState extends State<CreateForm> {
},
),
suggestions: suggestionsState.suggestions["amounts"],
suggestionsState: suggestionsState,
),
buildPage(
pageIndex: 2,
title: "What's this for?",
description: userOwesFriend
? "Why do you owe ${_textControllers[0].text} ${settingsState.selectedCurrency}${_textControllers[1].text}?"
: "Why does ${_textControllers[0].text} owe you ${settingsState.selectedCurrency}${_textControllers[1].text}?",
textField: TextFormField(
controller: _textControllers[2],
autofocus: true,
textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(
prefixIcon: Icon(Icons.message),
),
validator: (value) {
if (value.isEmpty) return 'Please enter a reason';
if (value.length > 21)
return 'Surpassed character limit. ${value.length}/21';
return null;
},
pageIndex: 2,
title: "What's this for?",
description: userOwesFriend
? "Why do you owe ${_textControllers[0].text} ${settingsState.selectedCurrency}${_textControllers[1].text}?"
: "Why does ${_textControllers[0].text} owe you ${settingsState.selectedCurrency}${_textControllers[1].text}?",
textField: TextFormField(
controller: _textControllers[2],
autofocus: true,
textCapitalization: TextCapitalization.sentences,
decoration: InputDecoration(
prefixIcon: Icon(Icons.message),
),
suggestions: suggestionsState.suggestions["descriptions"]),
validator: (value) {
if (value.isEmpty) return 'Please enter a reason';
if (value.length > 21)
return 'Surpassed character limit. ${value.length}/21';
return null;
},
),
suggestions: suggestionsState.suggestions["descriptions"],
suggestionsState: suggestionsState,
),
];
return _pages;
}
Expand All @@ -284,7 +333,7 @@ class _CreateFormState extends State<CreateForm> {
child: ChangeNotifierProvider(
create: (context) => Suggestions(),
child: Consumer<Suggestions>(
builder: (_, suggestionsState, __) => PageView(
builder: (context, suggestionsState, __) => PageView(
controller: _pageViewController,
physics: NeverScrollableScrollPhysics(),
children: buildPages(
Expand Down
46 changes: 25 additions & 21 deletions lib/providers/suggestionsState.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,11 @@ import 'dart:math';
class Suggestions extends ChangeNotifier {
static const int _NAMES_LEN = 3;
static const int _DESCRIPTIONS_LEN = 3;
static Map<String, dynamic> _defaultSuggestions = {
Map<String, dynamic> _savedSuggestions = {
"names": [],
"amounts": ["5", "10", "20", "50"],
"descriptions": {"Food": 0, "Rent": 0, "A job well done": 0}
"descriptions": Map<String, int>()
};
Map<String, dynamic> _savedSuggestions;

Suggestions() {
_fetchFromDatabase();
Expand All @@ -20,28 +19,19 @@ class Suggestions extends ChangeNotifier {
Map<String, List<String>> get suggestions {
/* only need to convert descriptions from freq map to list */
Map<String, List<String>> formatted = Map();
try {
formatted["names"] = _savedSuggestions["names"].cast<String>();
formatted["amounts"] = _savedSuggestions["amounts"].cast<String>();
formatted["descriptions"] = _freqMapToList(
_savedSuggestions["descriptions"],
_DESCRIPTIONS_LEN,
);
} catch (e) {
formatted["names"] = _defaultSuggestions["names"].cast<String>();
formatted["amounts"] = _defaultSuggestions["amounts"].cast<String>();
formatted["descriptions"] = _freqMapToList(
_defaultSuggestions["descriptions"],
_DESCRIPTIONS_LEN,
);
}
formatted["names"] = _savedSuggestions["names"].cast<String>();
formatted["amounts"] = _savedSuggestions["amounts"].cast<String>();
formatted["descriptions"] = _freqMapToList(
_savedSuggestions["descriptions"],
_DESCRIPTIONS_LEN,
);
return formatted;
}

void _fetchFromDatabase() async {
Map<String, dynamic> savedSuggestions =
Map<String, dynamic> suggestions =
await SuggestionsController.fetchSuggestions();
_savedSuggestions = savedSuggestions ?? _defaultSuggestions;
if (suggestions != null) _savedSuggestions = suggestions;
notifyListeners();
}

Expand All @@ -68,7 +58,7 @@ class Suggestions extends ChangeNotifier {
if (descriptionsMap.containsKey(description))
descriptionsMap[description]++;
else {
if (descriptionsMap.length >= 9) {
if (descriptionsMap.length >= 6) {
/* remove the least frequent entry */
int minimum = descriptionsMap.values.reduce(min);
for (String key in descriptionsMap.keys) {
Expand All @@ -82,9 +72,23 @@ class Suggestions extends ChangeNotifier {
}
}

void removeName(String name) {
_savedSuggestions["names"].remove(name);
notifyListeners();
SuggestionsController.updateSuggestions(_savedSuggestions);
}

void removeDescription(String description) {
_savedSuggestions["descriptions"].remove(description);
print(_savedSuggestions["descriptions"]);
notifyListeners();
SuggestionsController.updateSuggestions(_savedSuggestions);
}

/// returns k most frequent items in frequency map
List<String> _freqMapToList(Map<String, int> map, int k) {
List<int> frequencies = map.values.toList();
k = min(k, frequencies.length);
frequencies.sort((a, b) => b.compareTo(a)); // descending
Map<String, int> frequencyMapCopy = Map.of(map);
List<String> descriptions = List();
Expand Down

0 comments on commit 97df53d

Please sign in to comment.