add websock functionality
This commit is contained in:
@@ -1,52 +1,305 @@
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:one_trip/api/models/recipe.dart';
|
||||
// import 'package:one_trip/api/models/user.dart';
|
||||
// import 'package:one_trip/pages/recipes_page/widgets/recipe_card_widget.dart';
|
||||
// import 'package:one_trip/widgets/text_entry_dialog.dart';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
// class RecipesPage extends StatefulWidget {
|
||||
// const RecipesPage({super.key});
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:one_trip/api/auth.dart';
|
||||
import 'package:one_trip/api/consts.dart';
|
||||
import 'package:one_trip/api/models/list.dart';
|
||||
import 'package:one_trip/api/models/listingredient.dart';
|
||||
import 'package:one_trip/api/models/user.dart';
|
||||
import 'package:one_trip/pages/list_page/widgets/listrow.dart';
|
||||
import 'package:one_trip/pages/list_page/widgets/search_recipes.dart';
|
||||
import 'package:one_trip/widgets/text_entry_dialog.dart';
|
||||
|
||||
// @override
|
||||
// State<RecipesPage> createState() => _RecipesPageState();
|
||||
// }
|
||||
class ListPage extends StatefulWidget {
|
||||
const ListPage({super.key});
|
||||
|
||||
// class _RecipesPageState extends State<RecipesPage> {
|
||||
// late Future<List<Recipe>> _recipes;
|
||||
// late User _userInfo;
|
||||
@override
|
||||
State<ListPage> createState() => _ListPageState();
|
||||
}
|
||||
|
||||
// Future<List<Recipe>> _fetchList() async {
|
||||
// User? userInfo = await User.getMe();
|
||||
// if (userInfo == null || userInfo.homegroup == null) {
|
||||
// return [];
|
||||
// }
|
||||
// _userInfo = userInfo;
|
||||
class _ListPageState extends State<ListPage> {
|
||||
ShoppingList? _list;
|
||||
late Future<bool> _isLoaded;
|
||||
User? _userInfo;
|
||||
WebSocket? _ws;
|
||||
|
||||
// List<Recipe> recipes = await Recipe.getList(_userInfo.homegroup!);
|
||||
// return recipes;
|
||||
// }
|
||||
Future<bool> _fetchList() async {
|
||||
User? userInfo = await User.getMe();
|
||||
_userInfo = userInfo;
|
||||
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// _recipes = _fetchList();
|
||||
// }
|
||||
if (userInfo == null || userInfo.homegroup == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// return FutureBuilder(
|
||||
// future: _recipes,
|
||||
// builder: (context, snapshot) {
|
||||
// if (snapshot.hasError) {
|
||||
// return Text(snapshot.error.toString());
|
||||
// } else if (snapshot.hasData &&
|
||||
// snapshot.connectionState == ConnectionState.done) {
|
||||
// return RecipeList(
|
||||
// recipes: snapshot.data!, homegroup: _userInfo.homegroup!);
|
||||
// } else {
|
||||
// return const Center(child: CircularProgressIndicator());
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
_list = await ShoppingList.get(userInfo.homegroup!);
|
||||
return true;
|
||||
}
|
||||
|
||||
void _connectSocket() async {
|
||||
String token = TokenSingleton().getToken();
|
||||
_ws = await WebSocket.connect("$baseWsURL/ws/",
|
||||
headers: {"Authorization": "Token $token"});
|
||||
|
||||
if (_ws == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_ws!.listen((event) async {
|
||||
Map<String, dynamic> json = jsonDecode(event);
|
||||
|
||||
if (json.keys.contains("type") && json["type"] == "recommend_update") {
|
||||
if (json["hash"] != _list.hashCode) {
|
||||
ShoppingList? newList = await ShoppingList.get(_list!.homegroup);
|
||||
|
||||
if (newList != null) {
|
||||
setState(() {
|
||||
_list = newList;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _sendUpdate() async {
|
||||
if (_ws == null) {
|
||||
return;
|
||||
}
|
||||
_ws!.add(jsonEncode({"type": "broadcast_update", "hash": _list.hashCode}));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (_ws != null) {
|
||||
_ws!.close();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_isLoaded = _fetchList();
|
||||
_connectSocket();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: _isLoaded,
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasError) {
|
||||
return Text(snapshot.error.toString());
|
||||
} else if (snapshot.hasData &&
|
||||
snapshot.connectionState == ConnectionState.done) {
|
||||
if (_userInfo == null) {
|
||||
return const Center(
|
||||
child: Text("Could not load user, try logging in again..."),
|
||||
);
|
||||
} else if (_userInfo!.homegroup == null) {
|
||||
return const Center(
|
||||
child: Text("You must be in a homegroup to use this feature"),
|
||||
);
|
||||
} else if (_list == null) {
|
||||
return const Center(
|
||||
child: Text("Issue loading list"),
|
||||
);
|
||||
} else {
|
||||
return ListArea(
|
||||
list: _list!,
|
||||
onAddOne: () async {
|
||||
String? itemName =
|
||||
await textEntryDialog(context, "Item Name", "Item");
|
||||
|
||||
if (itemName == null || itemName == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
ListIngredient? newIngredient =
|
||||
await ListIngredient.create(itemName, _list!.homegroup);
|
||||
if (newIngredient == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ShoppingList? newList =
|
||||
await ShoppingList.get(_list!.homegroup);
|
||||
|
||||
if (newList != null) {
|
||||
setState(() {
|
||||
_list = newList;
|
||||
});
|
||||
}
|
||||
|
||||
_sendUpdate();
|
||||
},
|
||||
onAddMany: () async {
|
||||
List<int>? selectedIDs = await searchRecipesDialog(context);
|
||||
|
||||
if (selectedIDs == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ShoppingList tempList = _list!;
|
||||
for (int id in selectedIDs) {
|
||||
ShoppingList? newList = await tempList.addRecipe(id);
|
||||
|
||||
if (newList != null) {
|
||||
tempList = newList;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_list = tempList;
|
||||
});
|
||||
|
||||
_sendUpdate();
|
||||
},
|
||||
onDelete: (ingredient) async {
|
||||
bool success = await ingredient.delete();
|
||||
if (success) {
|
||||
// ShoppingList? newList =
|
||||
// await _list!.patch(updates: _list!.updates + 1);
|
||||
|
||||
ShoppingList? newList =
|
||||
await ShoppingList.get(_list!.homegroup);
|
||||
|
||||
setState(() {
|
||||
_list = newList;
|
||||
});
|
||||
|
||||
_sendUpdate();
|
||||
}
|
||||
|
||||
return success;
|
||||
},
|
||||
onUpdate: (ingredient, {inCart, name}) async {
|
||||
ListIngredient? updated =
|
||||
await ingredient.patch(name: name, inCart: inCart);
|
||||
if (updated != null) {
|
||||
ShoppingList? newList =
|
||||
await ShoppingList.get(_list!.homegroup);
|
||||
|
||||
setState(() {
|
||||
_list = newList;
|
||||
});
|
||||
|
||||
_sendUpdate();
|
||||
}
|
||||
},
|
||||
onClear: () async {
|
||||
ShoppingList? newList = await _list!.clear();
|
||||
|
||||
if (newList != null) {
|
||||
setState(() {
|
||||
_list = newList;
|
||||
});
|
||||
}
|
||||
|
||||
_sendUpdate();
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ListArea extends StatelessWidget {
|
||||
final ShoppingList list;
|
||||
final Function() onAddOne;
|
||||
final Function() onAddMany;
|
||||
final Function() onClear;
|
||||
final Future<bool> Function(ListIngredient ingredient) onDelete;
|
||||
final Function(ListIngredient ingredient, {String? name, bool? inCart})
|
||||
onUpdate;
|
||||
const ListArea({
|
||||
super.key,
|
||||
required this.list,
|
||||
required this.onAddOne,
|
||||
required this.onAddMany,
|
||||
required this.onDelete,
|
||||
required this.onUpdate,
|
||||
required this.onClear,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
key: UniqueKey(),
|
||||
itemCount: list.ingredients.length,
|
||||
padding: const EdgeInsets.all(8),
|
||||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
return ListRow(
|
||||
ingredient: list.ingredients[index],
|
||||
onToggle: (value) {
|
||||
onUpdate(list.ingredients[index], inCart: value);
|
||||
},
|
||||
apiRemove: (ingredient) async => await onDelete(ingredient),
|
||||
index: index,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
ButtonStyle buttonStyle = ButtonStyle(
|
||||
fixedSize: MaterialStatePropertyAll(
|
||||
Size(constraints.maxWidth / 3, 45),
|
||||
),
|
||||
shape: MaterialStateProperty.all(
|
||||
const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(
|
||||
bottom: Radius.zero, top: Radius.circular(10)),
|
||||
),
|
||||
),
|
||||
padding: const MaterialStatePropertyAll(EdgeInsets.zero));
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
style: buttonStyle,
|
||||
onPressed: () => onAddMany(),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [Icon(Icons.post_add), Text("Add Recipes")],
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: buttonStyle,
|
||||
onPressed: () => onAddOne(),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [Icon(Icons.add), Text("Add Item")],
|
||||
),
|
||||
),
|
||||
ElevatedButton(
|
||||
style: buttonStyle.copyWith(
|
||||
backgroundColor: MaterialStatePropertyAll(
|
||||
Theme.of(context).colorScheme.error),
|
||||
foregroundColor: MaterialStatePropertyAll(
|
||||
Theme.of(context).colorScheme.onError),
|
||||
),
|
||||
onPressed: () => onClear(),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: const [Icon(Icons.delete), Text("Clear List")],
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
85
one_trip/lib/pages/list_page/widgets/listrow.dart
Normal file
85
one_trip/lib/pages/list_page/widgets/listrow.dart
Normal file
@@ -0,0 +1,85 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:one_trip/api/models/listingredient.dart';
|
||||
|
||||
class ListRow extends StatefulWidget {
|
||||
final ListIngredient ingredient;
|
||||
final Future<bool> Function(ListIngredient ingredient) apiRemove;
|
||||
final Function(bool value) onToggle;
|
||||
final int index;
|
||||
const ListRow({
|
||||
super.key,
|
||||
required this.ingredient,
|
||||
required this.onToggle,
|
||||
required this.index,
|
||||
required this.apiRemove,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ListRow> createState() => _ListRowState();
|
||||
}
|
||||
|
||||
class _ListRowState extends State<ListRow> {
|
||||
double dismissAmount = 0.0;
|
||||
bool willDismiss = false;
|
||||
final UniqueKey key = UniqueKey();
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ListRow oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => widget.onToggle(!widget.ingredient.inCart),
|
||||
child: Dismissible(
|
||||
key: key,
|
||||
direction: DismissDirection.endToStart,
|
||||
onUpdate: (details) => setState(() {
|
||||
dismissAmount = details.progress;
|
||||
willDismiss = details.reached;
|
||||
}),
|
||||
confirmDismiss: (direction) async =>
|
||||
await widget.apiRemove(widget.ingredient),
|
||||
background: Container(
|
||||
color: Color.lerp(
|
||||
Colors.transparent, Colors.red, min(dismissAmount * 2.5, 1)),
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: SizedBox(
|
||||
width: 45,
|
||||
child: Icon(
|
||||
Icons.delete,
|
||||
size: min(27.5 * dismissAmount + 20, 35),
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: widget.ingredient.inCart,
|
||||
onChanged: (value) => widget.onToggle(value!),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
// _ingredient.name,
|
||||
widget.ingredient.name,
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
decoration: widget.ingredient.inCart
|
||||
? TextDecoration.lineThrough
|
||||
: null,
|
||||
color: widget.ingredient.inCart ? Colors.green : null,
|
||||
),
|
||||
),
|
||||
),
|
||||
// IconButton(onPressed: () {}, icon: const Icon(Icons.edit)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
168
one_trip/lib/pages/list_page/widgets/search_recipes.dart
Normal file
168
one_trip/lib/pages/list_page/widgets/search_recipes.dart
Normal file
@@ -0,0 +1,168 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:one_trip/api/models/recipe.dart';
|
||||
import 'package:one_trip/api/searchresult.dart';
|
||||
import 'package:one_trip/theme.dart';
|
||||
import 'package:one_trip/widgets/pagination_listview.dart';
|
||||
|
||||
class SearchRecipesDialog extends StatefulWidget {
|
||||
const SearchRecipesDialog({super.key});
|
||||
|
||||
@override
|
||||
State<SearchRecipesDialog> createState() => _SearchRecipesDialogState();
|
||||
}
|
||||
|
||||
class _SearchRecipesDialogState extends State<SearchRecipesDialog> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
ListViewState _listState = ListViewState.inactive;
|
||||
List<int> selectedIDs = [];
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Search your Recipes",
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const Divider(),
|
||||
TextFormField(
|
||||
controller: _searchController,
|
||||
textInputAction: TextInputAction.search,
|
||||
onFieldSubmitted: (value) {
|
||||
setState(() {
|
||||
_listState = ListViewState.changed;
|
||||
});
|
||||
},
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_listState = ListViewState.inactive;
|
||||
});
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
label: const Text("Search"),
|
||||
isDense: true,
|
||||
suffix: IconButton(
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_listState = ListViewState.changed;
|
||||
});
|
||||
|
||||
// https://flutterigniter.com/dismiss-keyboard-form-lose-focus/
|
||||
FocusScopeNode currentFocus = FocusScope.of(context);
|
||||
if (!currentFocus.hasPrimaryFocus) {
|
||||
currentFocus.unfocus();
|
||||
}
|
||||
},
|
||||
icon: const Icon(Icons.search),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
LayoutBuilder(
|
||||
builder: (builder, constraints) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints.expand(
|
||||
width: constraints.maxWidth - 8,
|
||||
height: 160,
|
||||
),
|
||||
child: PaginationListView(
|
||||
state: _listState,
|
||||
shrinkWrap: true,
|
||||
itemBuilder: (context, data) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_listState = ListViewState.inUse;
|
||||
if (selectedIDs.contains(data.id)) {
|
||||
selectedIDs.remove(data.id);
|
||||
} else {
|
||||
selectedIDs.add(data.id);
|
||||
}
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
color: selectedIDs.contains(data.id)
|
||||
? Theme.of(context).colorScheme.secondary
|
||||
: null,
|
||||
child: Text(data.name),
|
||||
),
|
||||
);
|
||||
},
|
||||
seperatorBuilder: (context, data) {
|
||||
return const Divider();
|
||||
},
|
||||
dataProvider: (int page) async {
|
||||
// SearchResult<SimpleUser> result =
|
||||
// await SimpleUser.search(_searchController.text, page);
|
||||
// List<dynamic> users = List<dynamic>.from(result.results);
|
||||
|
||||
// if (result.next == null) {
|
||||
// users.add(null);
|
||||
// }
|
||||
|
||||
// return users;
|
||||
|
||||
SearchResult<Recipe> result =
|
||||
await Recipe.search(_searchController.text, page);
|
||||
List<dynamic> recipes =
|
||||
List<dynamic>.from(result.results);
|
||||
|
||||
if (result.next == null) {
|
||||
recipes.add(null);
|
||||
}
|
||||
|
||||
return recipes;
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("Cancel"),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context, selectedIDs),
|
||||
child: const Text("Done")),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<int>?> searchRecipesDialog(BuildContext context) async {
|
||||
List<int>? selectedIDs = await showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return Dialog(
|
||||
child: ScrollConfiguration(
|
||||
behavior: MyBehavior(), child: const SearchRecipesDialog()),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return selectedIDs;
|
||||
}
|
||||
Reference in New Issue
Block a user