Added option to delete, edit and reorder categories.
NOTE: The click action is removed, if the user doesn't have the required permission and the drag-handle is not shown. BUG: Editing the list (in any way, other than reordering it), wont reload the list - hopefully this will be addressed later, by caching the categories (or removing them from the cache) and auto-rebuilding on cache-change
This commit is contained in:
parent
b73362c101
commit
1ba36ff9f0
5 changed files with 760 additions and 250 deletions
|
@ -372,6 +372,23 @@ class RoomCategory {
|
|||
name: json['title'],
|
||||
color: colorFromString(json['color']));
|
||||
}
|
||||
|
||||
static List<ColorSwatch<int>> listColors() {
|
||||
return [
|
||||
"red",
|
||||
"green",
|
||||
"yellow",
|
||||
"blue",
|
||||
"aqua",
|
||||
"purple",
|
||||
"red-acc",
|
||||
"green-acc",
|
||||
"yellow-acc",
|
||||
"blue-acc",
|
||||
"aqua-acc",
|
||||
"purple-acc",
|
||||
].map((txt) => colorFromString(txt)).toList();
|
||||
}
|
||||
}
|
||||
|
||||
ColorSwatch<int> colorFromString(String text) {
|
||||
|
@ -404,3 +421,45 @@ ColorSwatch<int> colorFromString(String text) {
|
|||
return Colors.purple;
|
||||
}
|
||||
}
|
||||
|
||||
String colorIdFromColor(ColorSwatch<int> color) {
|
||||
if (color == Colors.redAccent) {
|
||||
return 'red-acc';
|
||||
}
|
||||
if (color == Colors.greenAccent) {
|
||||
return 'green-acc';
|
||||
}
|
||||
if (color == Colors.yellowAccent) {
|
||||
return 'yellow-acc';
|
||||
}
|
||||
if (color == Colors.blueAccent) {
|
||||
return 'blue-acc';
|
||||
}
|
||||
if (color == Colors.tealAccent) {
|
||||
return 'teal-acc';
|
||||
}
|
||||
if (color == Colors.purpleAccent) {
|
||||
return 'purple-acc';
|
||||
}
|
||||
|
||||
if (color == Colors.red) {
|
||||
return 'red';
|
||||
}
|
||||
if (color == Colors.green) {
|
||||
return 'green';
|
||||
}
|
||||
if (color == Colors.yellow) {
|
||||
return 'yellow';
|
||||
}
|
||||
if (color == Colors.blue) {
|
||||
return 'blue';
|
||||
}
|
||||
if (color == Colors.teal) {
|
||||
return 'teal';
|
||||
}
|
||||
if (color == Colors.purple) {
|
||||
return 'purple';
|
||||
}
|
||||
|
||||
return 'purple';
|
||||
}
|
||||
|
|
|
@ -1,9 +1,4 @@
|
|||
{
|
||||
"helloWorld": "Hello World!",
|
||||
"@helloWorld": {
|
||||
"description": "The conventional newborn programmer greeting"
|
||||
},
|
||||
|
||||
"welcomeTitle": "Welcome to Outbag",
|
||||
"@welcomeTitle": {
|
||||
"description": "Title shown on welcome screen"
|
||||
|
@ -291,7 +286,7 @@
|
|||
"errorAccountDeletion": "Your account no longer exists",
|
||||
|
||||
"errorNoSuchAccount": "Account does not exist",
|
||||
"errorUsernameTaken": "Already with that username already exists",
|
||||
"errorUsernameTaken": "Account with that username already exists",
|
||||
"errorNoSuchRoom": "Room does not exist",
|
||||
"errorRoomIdTaken": "Room with that ID already exists",
|
||||
|
||||
|
@ -318,5 +313,28 @@
|
|||
"errorInvalidToken": "Invalid Token",
|
||||
|
||||
"errorServerDoesntExist": "Server does not exist",
|
||||
"errorInvalidServerToken": "Server token invalid"
|
||||
"errorInvalidServerToken": "Server token invalid",
|
||||
|
||||
"editCategoryShort": "Edit",
|
||||
"editCategory": "Edit Category",
|
||||
"editCategoryLong": "Change the category color or name",
|
||||
"newCategory": "New Category",
|
||||
"newCategoryLong": "Create a new category",
|
||||
"newCategoryShort": "Create",
|
||||
"deleteCategory": "Delete Category",
|
||||
"deleteCategoryConfirm": "Do you really want to remove the category named {category}",
|
||||
"@deleteCategoryConfirm": {
|
||||
"placeholders": {
|
||||
"category": {
|
||||
"type": "String",
|
||||
"example": "fruit"
|
||||
}
|
||||
}
|
||||
},
|
||||
"deleteCategoryLong": "Remove category and unlink items",
|
||||
"changeCategoryColor": "Change category color",
|
||||
"chooseCategoryColor": "Choose a color for your category",
|
||||
"inputCategoryNameLabel": "Category Name",
|
||||
"inputCategoryNameHint": "Name the category",
|
||||
"inputCategoryNameHelp": "Categories can be used to sort your shopping list"
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
|||
import 'package:outbag_app/backend/themes.dart';
|
||||
import 'package:outbag_app/backend/user.dart';
|
||||
import 'package:outbag_app/backend/request.dart';
|
||||
import 'package:outbag_app/screens/room/categories/edit.dart';
|
||||
|
||||
import 'package:outbag_app/tools/fetch_wrapper.dart';
|
||||
|
||||
|
@ -247,6 +248,19 @@ class _OutbagAppState extends State {
|
|||
EditRoomPermissionSetPage(
|
||||
state.params['server'] ?? '',
|
||||
state.params['id'] ?? '')),
|
||||
GoRoute(
|
||||
name: 'new-category',
|
||||
path: 'new-category',
|
||||
builder: (context, state)=>EditCategoryPage(
|
||||
state.params['server'] ?? '',
|
||||
state.params['id'] ?? '')),
|
||||
GoRoute(
|
||||
name: 'edit-category',
|
||||
path: 'edit-category/:cid',
|
||||
builder: (context, state)=>EditCategoryPage(
|
||||
state.params['server'] ?? '',
|
||||
state.params['id'] ?? '',
|
||||
id: int.tryParse(state.params['cid'] ?? ''))),
|
||||
])
|
||||
]),
|
||||
]),
|
||||
|
|
248
lib/screens/room/categories/edit.dart
Normal file
248
lib/screens/room/categories/edit.dart
Normal file
|
@ -0,0 +1,248 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:outbag_app/backend/request.dart';
|
||||
import 'package:outbag_app/backend/room.dart';
|
||||
import 'package:outbag_app/backend/user.dart';
|
||||
import 'package:outbag_app/tools/fetch_wrapper.dart';
|
||||
import 'package:outbag_app/tools/snackbar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||||
import 'dart:math';
|
||||
|
||||
class EditCategoryPage extends StatefulWidget {
|
||||
final String server;
|
||||
final String tag;
|
||||
int? id;
|
||||
|
||||
EditCategoryPage(this.server, this.tag, {super.key, this.id});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _EditCategoryPageState();
|
||||
}
|
||||
|
||||
class _EditCategoryPageState extends State<EditCategoryPage> {
|
||||
final TextEditingController _ctrName = TextEditingController();
|
||||
ColorSwatch<int> _ctrColor = Colors.purple;
|
||||
|
||||
bool showSpinner = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => fetchCategory());
|
||||
}
|
||||
|
||||
void fetchCategory() {
|
||||
final user = context.read<User>();
|
||||
|
||||
// TODO: load cached rooms
|
||||
|
||||
doNetworkRequest(ScaffoldMessenger.of(context),
|
||||
req: () => postWithCreadentials(
|
||||
credentials: user,
|
||||
target: user.server,
|
||||
path: 'getCategory',
|
||||
body: {
|
||||
'room': widget.tag,
|
||||
'server': widget.server,
|
||||
'listCatID': widget.id}),
|
||||
onOK: (json) {
|
||||
setState(() {
|
||||
_ctrName.text = json['data']['title'];
|
||||
_ctrColor = colorFromString(json['data']['color']);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context)
|
||||
.textTheme
|
||||
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
|
||||
|
||||
double width = MediaQuery.of(context).size.width;
|
||||
double height = MediaQuery.of(context).size.height;
|
||||
double smallest = min(min(width, height), 400);
|
||||
|
||||
return showSpinner
|
||||
? Scaffold(
|
||||
body: Center(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const CircularProgressIndicator(),
|
||||
Text(AppLocalizations.of(context)!.loading,
|
||||
style: textTheme.titleLarge),
|
||||
])))
|
||||
: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text((widget.id == null)
|
||||
? AppLocalizations.of(context)!.newCategory
|
||||
: AppLocalizations.of(context)!.editCategory),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 400),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
IconButton(
|
||||
icon: Icon(Icons.square_rounded,
|
||||
size: 48.0, color: _ctrColor),
|
||||
tooltip: AppLocalizations.of(context)!
|
||||
.changeCategoryColor,
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
title: Text(
|
||||
AppLocalizations.of(context)!
|
||||
.chooseCategoryColor),
|
||||
actions: const [],
|
||||
content: SizedBox(
|
||||
width: smallest * 0.3 * 3,
|
||||
height: smallest * 0.3 * 3,
|
||||
child: GridView.count(
|
||||
crossAxisCount: 3,
|
||||
children: RoomCategory
|
||||
.listColors()
|
||||
.map((color) {
|
||||
return GridTile(
|
||||
child: IconButton(
|
||||
icon: Icon(
|
||||
Icons
|
||||
.square_rounded,
|
||||
color:
|
||||
color,
|
||||
size: 48.0),
|
||||
// do not display tooltip for now
|
||||
// as it is hard to translate
|
||||
// and the tooltip prevented the click event,
|
||||
// when clicked on the tooltip bar
|
||||
// tooltip:icon.text,
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_ctrColor =
|
||||
color;
|
||||
});
|
||||
Navigator.of(
|
||||
ctx)
|
||||
.pop();
|
||||
}));
|
||||
}).toList())),
|
||||
));
|
||||
},
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: TextField(
|
||||
controller: _ctrName,
|
||||
keyboardType: TextInputType.name,
|
||||
decoration: InputDecoration(
|
||||
prefixIcon: const Icon(Icons.badge),
|
||||
labelText: AppLocalizations.of(context)!
|
||||
.inputCategoryNameLabel,
|
||||
hintText: AppLocalizations.of(context)!
|
||||
.inputCategoryNameHint,
|
||||
helperText: AppLocalizations.of(context)!
|
||||
.inputCategoryNameHelp,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
))))),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () async {
|
||||
final scaffMgr = ScaffoldMessenger.of(context);
|
||||
final router = GoRouter.of(context);
|
||||
final trans = AppLocalizations.of(context);
|
||||
|
||||
// name may not be empty
|
||||
if (_ctrName.text.isEmpty) {
|
||||
showSimpleSnackbar(scaffMgr,
|
||||
text: trans!.errorNoRoomName, action: trans.ok);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
showSpinner = true;
|
||||
});
|
||||
|
||||
final user = context.read<User>();
|
||||
final color = colorIdFromColor(_ctrColor);
|
||||
|
||||
if (widget.id == null) {
|
||||
await doNetworkRequest(scaffMgr,
|
||||
req: () => postWithCreadentials(
|
||||
target: user.server,
|
||||
credentials: user,
|
||||
path: 'addCategory',
|
||||
body: {
|
||||
'room': widget.tag,
|
||||
'server': widget.server,
|
||||
'title': _ctrName.text,
|
||||
'color': color
|
||||
}),
|
||||
onOK: (body) async {
|
||||
final id = body['data']['catID'];
|
||||
|
||||
final cat = RoomCategory(
|
||||
id: id, name: _ctrName.text, color: _ctrColor);
|
||||
// TODO: cache category
|
||||
|
||||
// go back
|
||||
router.pop();
|
||||
return;
|
||||
},
|
||||
after: () {
|
||||
setState(() {
|
||||
showSpinner = false;
|
||||
});
|
||||
});
|
||||
} else {
|
||||
await doNetworkRequest(scaffMgr,
|
||||
req: () => postWithCreadentials(
|
||||
target: user.server,
|
||||
credentials: user,
|
||||
path: 'changeCategory',
|
||||
body: {
|
||||
'room': widget.tag,
|
||||
'server': widget.server,
|
||||
'title': _ctrName.text,
|
||||
'listCatID': widget.id,
|
||||
'color': color
|
||||
}),
|
||||
onOK: (body) async {
|
||||
final cat = RoomCategory(
|
||||
id: widget.id!,
|
||||
name: _ctrName.text,
|
||||
color: _ctrColor);
|
||||
// TODO: cache category
|
||||
|
||||
// go back
|
||||
router.pop();
|
||||
return;
|
||||
},
|
||||
after: () {
|
||||
setState(() {
|
||||
showSpinner = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
label: Text((widget.id == null)
|
||||
? AppLocalizations.of(context)!.newCategoryShort
|
||||
: AppLocalizations.of(context)!.editCategoryShort),
|
||||
icon: Icon((widget.id == null)
|
||||
? Icons.add : Icons.edit)),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:outbag_app/backend/permissions.dart';
|
||||
import 'package:outbag_app/backend/request.dart';
|
||||
import 'package:outbag_app/backend/room.dart';
|
||||
import 'package:outbag_app/backend/user.dart';
|
||||
|
@ -51,33 +53,202 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context)
|
||||
.textTheme
|
||||
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
|
||||
|
||||
return Scaffold(
|
||||
body: ReorderableListView.builder(
|
||||
buildDefaultDragHandles: false,
|
||||
itemBuilder: (context, index) {
|
||||
final item = list[index];
|
||||
|
||||
return ListTile(
|
||||
key: Key('cat-${item.id}'),
|
||||
leading: Icon(Icons.square_rounded, color: item.color),
|
||||
trailing: ((widget.info?.isAdmin ?? false) ||
|
||||
(widget.info?.isOwner ?? false) ||
|
||||
((widget.info?.permissions)! &
|
||||
RoomPermission.editRoomContent !=
|
||||
0))
|
||||
? ReorderableDragStartListener(
|
||||
index: index,
|
||||
child: const Icon(Icons.drag_handle)
|
||||
)
|
||||
: null,
|
||||
title: Text(item.name),
|
||||
onTap: () {
|
||||
// TODO show edit category popup
|
||||
// NOTE: maybe use ModalBottomSheet
|
||||
// and show delete button in there
|
||||
|
||||
if (!((widget.info?.isAdmin ?? false) ||
|
||||
(widget.info?.isOwner ?? false) ||
|
||||
((widget.info?.permissions)! &
|
||||
RoomPermission.editRoomContent !=
|
||||
0))) {
|
||||
// user is not allowed to edit or delete categories
|
||||
return;
|
||||
}
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
builder: (context) => BottomSheet(
|
||||
builder: (context) => Column(children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.square_rounded,
|
||||
size: 48.0, color: item.color),
|
||||
Text(item.name, style: textTheme.titleLarge)
|
||||
],
|
||||
)),
|
||||
// edit category
|
||||
ListTile(
|
||||
leading: const Icon(Icons.edit),
|
||||
title: Text(AppLocalizations.of(context)!.editCategory),
|
||||
subtitle:
|
||||
Text(AppLocalizations.of(context)!.editCategoryLong),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
// close the modal bottom sheet
|
||||
// so the user returns to the list,
|
||||
// when leaving the category editor
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// launch category editor
|
||||
context.pushNamed('edit-category', params: {
|
||||
'server': widget.room!.serverTag,
|
||||
'id': widget.room!.id,
|
||||
'cid': item.id.toString()
|
||||
});
|
||||
},
|
||||
),
|
||||
// delete category
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete),
|
||||
title: Text(AppLocalizations.of(context)!.deleteCategory),
|
||||
subtitle: Text(
|
||||
AppLocalizations.of(context)!.deleteCategoryLong),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () {
|
||||
// show popup
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (ctx) => AlertDialog(
|
||||
icon: const Icon(Icons.delete),
|
||||
title: Text(AppLocalizations.of(context)!
|
||||
.deleteCategory),
|
||||
content: Text(AppLocalizations.of(context)!
|
||||
.deleteCategoryConfirm(item.name)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
// close popup
|
||||
Navigator.of(ctx).pop();
|
||||
},
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
// send request
|
||||
final scaffMgr =
|
||||
ScaffoldMessenger.of(ctx);
|
||||
// popup context
|
||||
final navInner = Navigator.of(ctx);
|
||||
// bottomsheet context
|
||||
final nav = Navigator.of(context);
|
||||
final user = context.read<User>();
|
||||
|
||||
doNetworkRequest(scaffMgr,
|
||||
req: () => postWithCreadentials(
|
||||
path: 'deleteCategory',
|
||||
target: user.server,
|
||||
body: {
|
||||
'room': widget.room?.id,
|
||||
'server':
|
||||
widget.room?.serverTag,
|
||||
'listCatID': item.id
|
||||
},
|
||||
credentials: user),
|
||||
onOK: (_) async {
|
||||
// TODO: remove cached category
|
||||
},
|
||||
after: () {
|
||||
// close popup
|
||||
navInner.pop();
|
||||
// close modal bottom sheet
|
||||
nav.pop();
|
||||
});
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!
|
||||
.deleteCategory),
|
||||
)
|
||||
],
|
||||
));
|
||||
},
|
||||
),
|
||||
]),
|
||||
onClosing: () {},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
itemCount: list.length,
|
||||
onReorder: (int start, int current) {},
|
||||
onReorder: (int oldIndex, int newIndex) {
|
||||
|
||||
if (!((widget.info?.isAdmin ?? false) ||
|
||||
(widget.info?.isOwner ?? false) ||
|
||||
((widget.info?.permissions)! &
|
||||
RoomPermission.editRoomContent !=
|
||||
0))) {
|
||||
// user is not allowed to edit or delete categories
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
if (oldIndex < newIndex) {
|
||||
newIndex -= 1;
|
||||
}
|
||||
final item = list.removeAt(oldIndex);
|
||||
list.insert(newIndex, item);
|
||||
|
||||
// network request
|
||||
final user = context.read<User>();
|
||||
doNetworkRequest(ScaffoldMessenger.of(context),
|
||||
req: () => postWithCreadentials(
|
||||
credentials: user,
|
||||
target: user.server,
|
||||
path: 'changeCategoriesOrder',
|
||||
body: {
|
||||
'room': widget.room?.id,
|
||||
'server': widget.room?.serverTag,
|
||||
'listCatIDs': list.map((item) => item.id).toList()
|
||||
}));
|
||||
});
|
||||
},
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
floatingActionButton: ((widget.info?.isAdmin ?? false) ||
|
||||
(widget.info?.isOwner ?? false) ||
|
||||
((widget.info?.permissions)! &
|
||||
RoomPermission.editRoomContent !=
|
||||
0))?FloatingActionButton.extended(
|
||||
icon: const Icon(Icons.add),
|
||||
label: Text(AppLocalizations.of(context)!.newCategoryShort),
|
||||
tooltip: AppLocalizations.of(context)!.newCategoryLong,
|
||||
onPressed: () {
|
||||
// TODO show new category popup
|
||||
// show new category popup
|
||||
context.pushNamed('new-category', params: {
|
||||
'server': widget.room!.serverTag,
|
||||
'id': widget.room!.id,
|
||||
});
|
||||
},
|
||||
),
|
||||
):null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue