remove item&product confirmation dialog

Because there currently is no way to auto-refresh the lists it will
appear as if nothing changed.
Switching to a different tab and back again will update the list though
This commit is contained in:
Jakob Meier 2024-02-23 10:16:57 +01:00
parent 384fbb0573
commit 13c071b8ca
No known key found for this signature in database
GPG key ID: 66BDC7E6A01A6152
8 changed files with 872 additions and 611 deletions

View file

@ -429,5 +429,12 @@
"newItemQueryEmpty": "Type to show quick access buttons", "newItemQueryEmpty": "Type to show quick access buttons",
"newItemQueryHint": "Type item or product name", "newItemQueryHint": "Type item or product name",
"newItemQuickAccessPrefix": "Create:", "newItemQuickAccessPrefix": "Create:",
"newItemQuickProduct": "product: {text}" "newItemQuickProduct": "product: {text}",
"deleteItem": "Delete Item",
"deleteItemConfirm": "Do you really want to remove the item named {item}",
"deleteProductTitle": "Delete Product",
"deleteProductSubtitle": "Remove the product from list of known products",
"deleteProduct": "Delete Product",
"deleteProductConfirm": "DO you really want to remove the product named {product}"
} }

View file

@ -123,38 +123,48 @@ class _HomePageState extends State<HomePage> {
) )
], ],
), ),
body: ListView.builder( body: Center(
itemCount: rooms.length, child: Padding(
itemBuilder: (ctx, i) { padding: const EdgeInsets.all(14),
final room = rooms[i]; child: ConstrainedBox(
return Card( constraints: const BoxConstraints(maxWidth: 600),
margin: const EdgeInsets.all(8.0), child: ListView.builder(
clipBehavior: Clip.antiAliasWithSaveLayer, itemCount: rooms.length,
semanticContainer: true, itemBuilder: (ctx, i) {
child: InkWell( final room = rooms[i];
onTap: () { return Card(
// open room margin: const EdgeInsets.all(8.0),
context.goNamed('room', clipBehavior: Clip.antiAliasWithSaveLayer,
params: {'server': room.serverTag, 'id': room.id}); semanticContainer: true,
}, child: InkWell(
onLongPress: () { onTap: () {
// open bottom sheet // open room
// NOTE: feature yet to be confirmed context.goNamed('room', params: {
}, 'server': room.serverTag,
child: Container( 'id': room.id
padding: const EdgeInsets.fromLTRB(10, 5, 5, 10), });
child: ListTile( },
title: Text(room.name), onLongPress: () {
visualDensity: const VisualDensity(vertical: 3), // open bottom sheet
subtitle: Text(room.description), // NOTE: feature yet to be confirmed
leading: AspectRatio( },
aspectRatio: 1 / 1, child: Container(
child: SvgPicture.asset("${room.icon?.img}"), padding:
), const EdgeInsets.fromLTRB(10, 5, 5, 10),
hoverColor: Colors.transparent, child: ListTile(
)))); title: Text(room.name),
}, visualDensity:
), const VisualDensity(vertical: 3),
subtitle: Text(room.description),
leading: AspectRatio(
aspectRatio: 1 / 1,
child:
SvgPicture.asset("${room.icon?.img}"),
),
hoverColor: Colors.transparent,
))));
},
)))),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: FloatingActionButton.extended(
label: Text(AppLocalizations.of(context)!.addRoom), label: Text(AppLocalizations.of(context)!.addRoom),
icon: const Icon(Icons.add), icon: const Icon(Icons.add),

View file

@ -138,7 +138,7 @@ class _EditItemPageState extends State<EditItemPage> {
child: Padding( child: Padding(
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 400), constraints: const BoxConstraints(maxWidth: 600),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,

View file

@ -33,303 +33,383 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
return SingleChildScrollView( return SingleChildScrollView(
child: Center( child: Center(
child: Column(children: [ child: Padding(
// room meta display
...(widget.room != null)
? [
Padding(
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
child: Column( child: ConstrainedBox(
children: [ constraints: const BoxConstraints(maxWidth: 600),
SvgPicture.asset( child: Column(children: [
(widget.room?.icon?.img)!, // room meta display
width: smallest * 0.2, ...(widget.room != null)
height: smallest * 0.2, ? [
), Padding(
Text( padding: const EdgeInsets.all(14),
widget.room?.name ?? '', child: Column(
style: textTheme.displayMedium, children: [
), SvgPicture.asset(
Text( (widget.room?.icon?.img)!,
'${widget.room?.id}@${widget.room?.serverTag}', width: smallest * 0.2,
style: textTheme.bodySmall, height: smallest * 0.2,
), ),
Text( Text(
widget.room?.description ?? '', widget.room?.name ?? '',
style: textTheme.bodyMedium, style: textTheme.displayMedium,
textAlign: TextAlign.center, ),
), Text(
Padding( '${widget.room?.id}@${widget.room?.serverTag}',
padding: const EdgeInsets.all(8), style: textTheme.bodySmall,
child: SegmentedButton<int>( ),
showSelectedIcon: true, Text(
multiSelectionEnabled: false, widget.room?.description ?? '',
emptySelectionAllowed: false, style: textTheme.bodyMedium,
segments: RoomVisibility.list().map((vis) { textAlign: TextAlign.center,
return ButtonSegment<int>( ),
value: vis.type, Padding(
label: Text(vis.text(context)), padding: const EdgeInsets.all(8),
icon: Icon(vis.icon)); child: SegmentedButton<int>(
}).toList(), showSelectedIcon: true,
onSelectionChanged: ((vset) { multiSelectionEnabled: false,
// check permission emptySelectionAllowed: false,
// only show confirm dialog when user segments:
// is admin, owner or has CHANGE_ADMIN permission RoomVisibility.list().map((vis) {
if (widget.info == null || return ButtonSegment<int>(
(!(widget.info?.isAdmin ?? false) && value: vis.type,
!(widget.info?.isOwner ?? false) && label: Text(vis.text(context)),
((widget.info?.permissions)! & icon: Icon(vis.icon));
RoomPermission.ota == }).toList(),
0))) { onSelectionChanged: ((vset) {
// action not permitted // check permission
// NOTE: no error dialog should be shown // only show confirm dialog when user
// because the action is supposed to be hidden // is admin, owner or has CHANGE_ADMIN permission
return; if (widget.info == null ||
} (!(widget.info?.isAdmin ??
false) &&
!(widget.info?.isOwner ??
false) &&
((widget.info
?.permissions)! &
RoomPermission
.ota ==
0))) {
// action not permitted
// NOTE: no error dialog should be shown
// because the action is supposed to be hidden
return;
}
final vis = RoomVisibility(vset.first); final vis =
showDialog( RoomVisibility(vset.first);
context: context, showDialog(
builder: (ctx) => AlertDialog( context: context,
title: Text(AppLocalizations.of(context)! builder: (ctx) => AlertDialog(
.changeRoomVisibilityTitle), title: Text(AppLocalizations
content: Text( .of(context)!
AppLocalizations.of(context)! .changeRoomVisibilityTitle),
.changeRoomVisibilitySubtitle( content: Text(AppLocalizations
vis.text(context))), .of(context)!
actions: [ .changeRoomVisibilitySubtitle(
TextButton( vis.text(
onPressed: () { context))),
context.pop(); actions: [
TextButton(
onPressed: () {
context.pop();
},
child: Text(
AppLocalizations.of(
context)!
.cancel),
),
FilledButton(
onPressed: () async {
final scaffMgr =
ScaffoldMessenger
.of(context);
final nav =
Navigator.of(
context);
final user = context
.read<User>();
doNetworkRequest(
scaffMgr,
req: () =>
postWithCreadentials(
path:
'setVisibility',
target: user
.server,
body: {
'room': widget
.room
?.id,
'server': (widget
.room
?.serverTag)!,
'visibility':
vset.first
},
credentials:
user),
onOK: (_) {
Room r = widget
.room!;
r.visibility =
vis;
r.toDisk();
},
after: () {
nav.pop();
});
},
child: Text(
AppLocalizations.of(
context)!
.ok),
)
],
));
}),
selected: {
(widget.room?.visibility?.type)!
}, },
child: Text( selectedIcon: Icon(
AppLocalizations.of(context)! (widget.room?.visibility?.icon)!),
.cancel), )),
), ],
FilledButton( ),
onPressed: () async { )
final scaffMgr = ]
ScaffoldMessenger.of(context); : [],
final nav = Navigator.of(context);
final user = context.read<User>();
doNetworkRequest(scaffMgr, Padding(
req: () => postWithCreadentials( padding: const EdgeInsets.all(14),
path: 'setVisibility', child: Column(
target: user.server, children: [
body: { // edit room meta button
'room': widget.room?.id, ...(widget.info != null &&
'server': (widget ((widget.info?.isAdmin ?? false) ||
.room?.serverTag)!, (widget.info?.isOwner ?? false) ||
'visibility': vset.first ((widget.info?.permissions)! &
}, RoomPermission.changeMeta !=
credentials: user), 0)))
onOK: (_) { ? [
Room r = widget.room!; ListTile(
r.visibility = vis; trailing:
r.toDisk(); const Icon(Icons.chevron_right),
}, title: Text(
after: () { AppLocalizations.of(context)!
nav.pop(); .editRoomMetadata),
}); subtitle: Text(
}, AppLocalizations.of(context)!
child: Text( .editRoomMetadataSubtitle),
AppLocalizations.of(context)!.ok), onTap: () {
) // show edit room screen
], context.goNamed('edit-room', params: {
)); 'server': (widget.room?.serverTag)!,
}), 'id': (widget.room?.id)!
selected: {(widget.room?.visibility?.type)!}, });
selectedIcon: Icon((widget.room?.visibility?.icon)!), },
)), ),
], ]
), : [],
) // open members view
] ListTile(
: [], trailing: const Icon(Icons.chevron_right),
title: Text(AppLocalizations.of(context)!
Padding( .showRoomMembers),
padding: const EdgeInsets.all(14), subtitle: Text(AppLocalizations.of(context)!
child: Column( .showRoomMembersSubtitle),
children: [ onTap: () {
// edit room meta button // open member view screen
...(widget.info != null && context.goNamed('room-members', params: {
((widget.info?.isAdmin ?? false) || 'server': (widget.room?.serverTag)!,
(widget.info?.isOwner ?? false) || 'id': (widget.room?.id)!
((widget.info?.permissions)! & });
RoomPermission.changeMeta != },
0))) ),
? [ // edit default member permission
ListTile( ...(widget.info != null &&
trailing: const Icon(Icons.chevron_right), ((widget.info?.isAdmin ?? false) ||
title: Text( (widget.info?.isOwner ?? false) ||
AppLocalizations.of(context)!.editRoomMetadata), ((widget.info?.permissions)! &
subtitle: Text(AppLocalizations.of(context)! RoomPermission.changeAdmin !=
.editRoomMetadataSubtitle), 0)))
onTap: () { ? [
// show edit room screen ListTile(
context.goNamed('edit-room', params: { trailing:
'server': (widget.room?.serverTag)!, const Icon(Icons.chevron_right),
'id': (widget.room?.id)! title: Text(
}); AppLocalizations.of(context)!
}, .editRoomPermissions),
), subtitle: Text(
] AppLocalizations.of(context)!
: [], .editRoomPermissionsSubtitle),
// open members view onTap: () {
ListTile( // show checkbox screen
trailing: const Icon(Icons.chevron_right), context.goNamed('room-permissions',
title: Text(AppLocalizations.of(context)!.showRoomMembers), params: {
subtitle:
Text(AppLocalizations.of(context)!.showRoomMembersSubtitle),
onTap: () {
// open member view screen
context.goNamed('room-members', params: {
'server': (widget.room?.serverTag)!,
'id': (widget.room?.id)!
});
},
),
// edit default member permission
...(widget.info != null &&
((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) ||
((widget.info?.permissions)! &
RoomPermission.changeAdmin !=
0)))
? [
ListTile(
trailing: const Icon(Icons.chevron_right),
title: Text(
AppLocalizations.of(context)!.editRoomPermissions),
subtitle: Text(AppLocalizations.of(context)!
.editRoomPermissionsSubtitle),
onTap: () {
// show checkbox screen
context.goNamed('room-permissions', params: {
'server': (widget.room?.serverTag)!,
'id': (widget.room?.id)!
});
},
),
]
: [],
...(widget.info != null &&
((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) ||
((widget.info?.permissions)! & RoomPermission.ota !=
0)))
? [
ListTile(
trailing: const Icon(Icons.chevron_right),
title:
Text(AppLocalizations.of(context)!.manageRoomOTA),
subtitle: Text(AppLocalizations.of(context)!
.manageRoomOTASubtitle),
onTap: () {
// show manage ota screen
context.goNamed('room-ota', params: {
'server': (widget.room?.serverTag)!,
'id': (widget.room?.id)!
});
},
),
ListTile(
trailing: const Icon(Icons.chevron_right),
title: Text(
AppLocalizations.of(context)!.manageRoomInvites),
subtitle: Text(AppLocalizations.of(context)!
.manageRoomInvitesSubtitle),
onTap: () {
// show manage ota screen
context.goNamed('room-invite', params: {
'server': (widget.room?.serverTag)!,
'id': (widget.room?.id)!
});
},
),
]
: [],
],
)),
...(widget.info != null)
? [
Padding(
padding: const EdgeInsets.all(8),
child: FilledButton.tonal(
child: Text(((widget.info?.isOwner)!)
? AppLocalizations.of(context)!.deleteRoom
: AppLocalizations.of(context)!.leaveRoom),
onPressed: () {
// show confirm dialog
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(((widget.info?.isOwner)!)
? AppLocalizations.of(context)!.deleteRoom
: AppLocalizations.of(context)!.leaveRoom),
content: Text(((widget.info?.isOwner)!)
? AppLocalizations.of(context)!
.deleteRoomConfirm
: AppLocalizations.of(context)!
.leaveRoomConfirm),
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);
final nav = Navigator.of(ctx);
final router = GoRouter.of(context);
final user = context.read<User>();
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
path: ((widget.info?.isOwner)!)
? 'deleteRoom'
: 'leaveRoom',
target: user.server,
body: {
'room': widget.room?.id,
'server': 'server':
(widget.room?.serverTag)!, (widget.room?.serverTag)!,
}, 'id': (widget.room?.id)!
credentials: user), });
onOK: (_) async { },
// try delete room from disk ),
try { ]
await widget.room?.removeDisk(); : [],
} catch (_) {} ...(widget.info != null &&
((widget.info?.isAdmin ?? false) ||
// go back home (widget.info?.isOwner ?? false) ||
router.pushReplacementNamed('home'); ((widget.info?.permissions)! &
}, RoomPermission.ota !=
after: () { 0)))
// close popup ? [
nav.pop(); ListTile(
trailing:
const Icon(Icons.chevron_right),
title: Text(
AppLocalizations.of(context)!
.manageRoomOTA),
subtitle: Text(
AppLocalizations.of(context)!
.manageRoomOTASubtitle),
onTap: () {
// show manage ota screen
context.goNamed('room-ota', params: {
'server': (widget.room?.serverTag)!,
'id': (widget.room?.id)!
}); });
}, },
),
ListTile(
trailing:
const Icon(Icons.chevron_right),
title: Text(
AppLocalizations.of(context)!
.manageRoomInvites),
subtitle: Text(
AppLocalizations.of(context)!
.manageRoomInvitesSubtitle),
onTap: () {
// show manage ota screen
context
.goNamed('room-invite', params: {
'server': (widget.room?.serverTag)!,
'id': (widget.room?.id)!
});
},
),
]
: [],
],
)),
...(widget.info != null)
? [
Padding(
padding: const EdgeInsets.all(8),
child: FilledButton.tonal(
child: Text(((widget.info?.isOwner)!) child: Text(((widget.info?.isOwner)!)
? AppLocalizations.of(context)! ? AppLocalizations.of(context)!
.deleteRoomShort .deleteRoom
: AppLocalizations.of(context)! : AppLocalizations.of(context)!
.leaveRoomShort), .leaveRoom),
) onPressed: () {
], // show confirm dialog
)); showDialog(
}, context: context,
)) builder: (ctx) => AlertDialog(
] title: Text(
: [], ((widget.info?.isOwner)!)
]))); ? AppLocalizations.of(
context)!
.deleteRoom
: AppLocalizations.of(
context)!
.leaveRoom),
content: Text(
((widget.info?.isOwner)!)
? AppLocalizations.of(
context)!
.deleteRoomConfirm
: AppLocalizations.of(
context)!
.leaveRoomConfirm),
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);
final nav =
Navigator.of(ctx);
final router =
GoRouter.of(context);
final user =
context.read<User>();
doNetworkRequest(scaffMgr,
req: () =>
postWithCreadentials(
path: ((widget
.info
?.isOwner)!)
? 'deleteRoom'
: 'leaveRoom',
target: user
.server,
body: {
'room': widget
.room
?.id,
'server': (widget
.room
?.serverTag)!,
},
credentials:
user),
onOK: (_) async {
// try delete room from disk
try {
await widget.room
?.removeDisk();
} catch (_) {}
// go back home
router
.pushReplacementNamed(
'home');
},
after: () {
// close popup
nav.pop();
});
},
child: Text(((widget
.info?.isOwner)!)
? AppLocalizations.of(
context)!
.deleteRoomShort
: AppLocalizations.of(
context)!
.leaveRoomShort),
)
],
));
},
))
]
: [],
])))));
} }
} }

View file

@ -61,178 +61,209 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
.apply(displayColor: Theme.of(context).colorScheme.onSurface); .apply(displayColor: Theme.of(context).colorScheme.onSurface);
return Scaffold( return Scaffold(
body: ReorderableListView.builder( body: Center(
buildDefaultDragHandles: false, child: Padding(
itemBuilder: (context, index) { padding: const EdgeInsets.all(14),
final item = list[index]; child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: ReorderableListView.builder(
buildDefaultDragHandles: false,
itemBuilder: (context, index) {
final item = list[index];
return ListTile( return ListTile(
key: Key('cat-${item.id}'), key: Key('cat-${item.id}'),
leading: Icon(Icons.square_rounded, color: item.color), leading: Icon(Icons.square_rounded, color: item.color),
trailing: ((widget.info?.isAdmin ?? false) || trailing: ((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) || (widget.info?.isOwner ?? false) ||
((widget.info?.permissions)! & ((widget.info?.permissions)! &
RoomPermission.editRoomContent != RoomPermission.editRoomContent !=
0)) 0))
? ReorderableDragStartListener( ? ReorderableDragStartListener(
index: index, child: const Icon(Icons.drag_handle)) index: index,
: null, child: const Icon(Icons.drag_handle))
title: Text(item.name), : null,
onTap: () { title: Text(item.name),
// TODO show edit category popup onTap: () {
// NOTE: maybe use ModalBottomSheet // TODO show edit category popup
// and show delete button in there // NOTE: maybe use ModalBottomSheet
// and show delete button in there
if (!((widget.info?.isAdmin ?? false) || if (!((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) || (widget.info?.isOwner ?? false) ||
((widget.info?.permissions)! & ((widget.info?.permissions)! &
RoomPermission.editRoomContent != RoomPermission.editRoomContent !=
0))) { 0))) {
// user is not allowed to edit or delete categories // user is not allowed to edit or delete categories
return; return;
} }
showModalBottomSheet( 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,
'category': 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, context: context,
builder: (ctx) => AlertDialog( builder: (context) => BottomSheet(
icon: const Icon(Icons.delete), 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,
'category': item.id.toString()
});
},
),
// delete category
ListTile(
leading: const Icon(Icons.delete),
title: Text(AppLocalizations.of(context)! title: Text(AppLocalizations.of(context)!
.deleteCategory), .deleteCategory),
content: Text(AppLocalizations.of(context)! subtitle: Text(AppLocalizations.of(context)!
.deleteCategoryConfirm(item.name)), .deleteCategoryLong),
actions: [ trailing: const Icon(Icons.chevron_right),
TextButton( onTap: () {
onPressed: () { // show popup
// close popup showDialog(
Navigator.of(ctx).pop(); context: context,
}, builder: (ctx) => AlertDialog(
child: Text( icon: const Icon(Icons.delete),
AppLocalizations.of(context)!.cancel), title: Text(
), AppLocalizations.of(context)!
FilledButton( .deleteCategory),
onPressed: () async { content: Text(
// send request AppLocalizations.of(context)!
final scaffMgr = .deleteCategoryConfirm(
ScaffoldMessenger.of(ctx); item.name)),
// popup context actions: [
final navInner = Navigator.of(ctx); TextButton(
// bottomsheet context onPressed: () {
final nav = Navigator.of(context); // close popup
final user = context.read<User>(); 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, doNetworkRequest(scaffMgr,
req: () => postWithCreadentials( req: () =>
path: 'deleteCategory', postWithCreadentials(
target: user.server, path:
body: { 'deleteCategory',
'room': widget.room?.id, target:
'server': user.server,
widget.room?.serverTag, body: {
'listCatID': item.id 'room': widget
}, .room?.id,
credentials: user), 'server': widget
onOK: (_) async { .room
// TODO: remove cached category ?.serverTag,
fetchCategories(); 'listCatID':
}, item.id
after: () { },
// close popup credentials:
navInner.pop(); user),
// close modal bottom sheet onOK: (_) async {
nav.pop(); // TODO: remove cached category
}); fetchCategories();
}, },
child: Text(AppLocalizations.of(context)! after: () {
.deleteCategory), // close popup
) navInner.pop();
], // close modal bottom sheet
)); nav.pop();
}, });
), },
]), child: Text(
onClosing: () {}, AppLocalizations.of(
), context)!
); .deleteCategory),
}, )
); ],
}, ));
itemCount: list.length, },
onReorder: (int oldIndex, int newIndex) { ),
if (!((widget.info?.isAdmin ?? false) || ]),
(widget.info?.isOwner ?? false) || onClosing: () {},
((widget.info?.permissions)! & RoomPermission.editRoomContent != ),
0))) { );
// user is not allowed to edit or delete categories },
return; );
} },
itemCount: list.length,
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(() { setState(() {
if (oldIndex < newIndex) { if (oldIndex < newIndex) {
newIndex -= 1; newIndex -= 1;
} }
final item = list.removeAt(oldIndex); final item = list.removeAt(oldIndex);
list.insert(newIndex, item); list.insert(newIndex, item);
// network request // network request
final user = context.read<User>(); final user = context.read<User>();
doNetworkRequest(ScaffoldMessenger.of(context), doNetworkRequest(ScaffoldMessenger.of(context),
req: () => postWithCreadentials( req: () => postWithCreadentials(
credentials: user, credentials: user,
target: user.server, target: user.server,
path: 'changeCategoriesOrder', path: 'changeCategoriesOrder',
body: { body: {
'room': widget.room?.id, 'room': widget.room?.id,
'server': widget.room?.serverTag, 'server': widget.room?.serverTag,
'listCatIDs': list.map((item) => item.id).toList() 'listCatIDs':
})); list.map((item) => item.id).toList()
}); }));
}, });
), },
)))),
floatingActionButton: (widget.info != null && floatingActionButton: (widget.info != null &&
((widget.info?.isAdmin ?? false) || ((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) || (widget.info?.isOwner ?? false) ||

View file

@ -194,110 +194,115 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: ListView(children: [ body: Center(
LabeledDivider(AppLocalizations.of(context)!.shoppingList), child: Padding(
ListView.builder( padding: const EdgeInsets.all(14),
shrinkWrap: true, child: ConstrainedBox(
physics: const ClampingScrollPhysics(), constraints: const BoxConstraints(maxWidth: 600),
itemCount: list.length, child: ListView(children: [
itemBuilder: (context, index) { LabeledDivider(AppLocalizations.of(context)!.shoppingList),
final item = list[index]; ListView.builder(
final cat = shrinkWrap: true,
categories[item.category] ?? RoomCategory.other(context); physics: const ClampingScrollPhysics(),
return ShoppingListItem( itemCount: list.length,
name: item.name, itemBuilder: (context, index) {
description: item.description, final item = list[index];
category: cat, final cat = categories[item.category] ??
key: Key(item.id.toString()), RoomCategory.other(context);
inCart: item.state != 0, return ShoppingListItem(
onDismiss: () { name: item.name,
// move to cart description: item.description,
item.state = 1; category: cat,
key: Key(item.id.toString()),
inCart: item.state != 0,
onDismiss: () {
// move to cart
item.state = 1;
setState(() { setState(() {
list.removeAt(index); list.removeAt(index);
cart.add(item); cart.add(item);
sortAll(); sortAll();
}); });
// network request // network request
// NOTE: for now we do not care if it is successfull, // NOTE: for now we do not care if it is successfull,
// because the shopping cart is supposed to work even // because the shopping cart is supposed to work even
// without network // without network
changeItemState(item); changeItemState(item);
}, },
onTap: () { onTap: () {
// TODO: show modal bottom sheet // TODO: show modal bottom sheet
// containing // containing
// - item info // - item info
// - option to view linked product (if available) // - option to view linked product (if available)
// - edit item (if allowed) // - edit item (if allowed)
// - delete item (if allowed) // - delete item (if allowed)
// - move to/from shopping cart? // - move to/from shopping cart?
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: (context) => ShoppingListItemInfo( builder: (context) => ShoppingListItemInfo(
products: products, products: products,
category: cat, category: cat,
info: widget.info, info: widget.info,
item: item, item: item,
room: widget.room!.id, room: widget.room!.id,
server: widget.room!.serverTag)); server: widget.room!.serverTag));
}); });
}, },
), ),
LabeledDivider(AppLocalizations.of(context)!.shoppingCart), LabeledDivider(AppLocalizations.of(context)!.shoppingCart),
ListView.builder( ListView.builder(
itemCount: cart.length, itemCount: cart.length,
shrinkWrap: true, shrinkWrap: true,
physics: const ClampingScrollPhysics(), physics: const ClampingScrollPhysics(),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final item = cart[index]; final item = cart[index];
final cat = final cat = categories[item.category] ??
categories[item.category] ?? RoomCategory.other(context); RoomCategory.other(context);
return ShoppingListItem( return ShoppingListItem(
name: item.name, name: item.name,
description: item.description, description: item.description,
category: cat, category: cat,
key: Key(item.id.toString()), key: Key(item.id.toString()),
inCart: item.state != 0, inCart: item.state != 0,
onDismiss: () { onDismiss: () {
// move back to list // move back to list
item.state = 0; item.state = 0;
setState(() { setState(() {
cart.removeAt(index); cart.removeAt(index);
list.add(item); list.add(item);
sortAll(); sortAll();
}); });
// network request // network request
// NOTE: for now we do not care if it is successfull, // NOTE: for now we do not care if it is successfull,
// because the shopping cart is supposed to work even // because the shopping cart is supposed to work even
// without network // without network
changeItemState(item); changeItemState(item);
}, },
onTap: () { onTap: () {
// show modal bottom sheet // show modal bottom sheet
// containing // containing
// - item info // - item info
// - option to view linked product (if available) // - option to view linked product (if available)
// - edit item (if allowed) // - edit item (if allowed)
// - delete item (if allowed) // - delete item (if allowed)
// - move to/from shopping cart? // - move to/from shopping cart?
showModalBottomSheet( showModalBottomSheet(
context: context, context: context,
builder: (context) => ShoppingListItemInfo( builder: (context) => ShoppingListItemInfo(
products: products, products: products,
category: cat, category: cat,
item: item, item: item,
info: widget.info, info: widget.info,
room: widget.room!.id, room: widget.room!.id,
server: widget.room!.serverTag)); server: widget.room!.serverTag));
}); });
}, },
) )
]), ])))),
floatingActionButton: (widget.info != null && floatingActionButton: (widget.info != null &&
((widget.info?.isAdmin ?? false) || ((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) || (widget.info?.isOwner ?? false) ||
@ -459,7 +464,60 @@ class ShoppingListItemInfo extends StatelessWidget {
AppLocalizations.of(context)!.deleteItemSubtitle), AppLocalizations.of(context)!.deleteItemSubtitle),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () { onTap: () {
// TODO: show confirm dialog // show popup
showDialog(
context: context,
builder: (ctx) => AlertDialog(
icon: const Icon(Icons.delete),
title: Text(
AppLocalizations.of(context)!.deleteItem),
content: Text(AppLocalizations.of(context)!
.deleteItemConfirm(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: 'deleteItem',
target: user.server,
body: {
'room': room,
'server': server,
'listItemID': item.id
},
credentials: user),
onOK: (_) async {
// TODO: remove cached item
},
after: () {
// close popup
navInner.pop();
// close modal bottom sheet
nav.pop();
});
},
child: Text(AppLocalizations.of(context)!
.deleteItem),
)
],
));
}), }),
] ]
: [], : [],

View file

@ -55,32 +55,37 @@ class _RoomProductsPageState extends State<RoomProductsPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: ListView.builder( body: Center(
itemCount: products.length, child: Padding(
itemBuilder: (context, index) { padding: const EdgeInsets.all(14),
final item = products[index]; child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: ListView.builder(
itemCount: products.length,
itemBuilder: (context, index) {
final item = products[index];
return ListTile( return ListTile(
title: Text(item.name), title: Text(item.name),
subtitle: Text(item.description), subtitle: Text(item.description),
onTap: () { onTap: () {
// NOTE: we could also show a bottom sheet here, // NOTE: we could also show a bottom sheet here,
// but because we need a seperate page/route either way // but because we need a seperate page/route either way
// (for viewing item-links and exploring the product-tree) // (for viewing item-links and exploring the product-tree)
// we might as well use the view-product page here // we might as well use the view-product page here
// NOTE: This might seem inconsistent, // NOTE: This might seem inconsistent,
// but you probably wont ever need to read a product description, // but you probably wont ever need to read a product description,
// where as reading the shopping item description, // where as reading the shopping item description,
// might be a good idea // might be a good idea
context.pushNamed('view-product', params: { context.pushNamed('view-product', params: {
'server': widget.room!.serverTag, 'server': widget.room!.serverTag,
'id': widget.room!.id, 'id': widget.room!.id,
'product': item.id.toString() 'product': item.id.toString()
}); });
}, },
); );
}, },
), )))),
floatingActionButton: (widget.info != null && floatingActionButton: (widget.info != null &&
((widget.info?.isAdmin ?? false) || ((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) || (widget.info?.isOwner ?? false) ||

View file

@ -207,6 +207,76 @@ class _ViewProductPageState extends State<ViewProductPage> {
}, },
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
), ),
...(info != null &&
((info?.isAdmin ?? false) ||
(info?.isOwner ?? false) ||
((info?.permissions)! & RoomPermission.editRoomContent !=
0)))
? [
// delete product
ListTile(
title: Text(AppLocalizations.of(context)!.deleteProductTitle),
subtitle:
Text(AppLocalizations.of(context)!.deleteProductSubtitle),
onTap: () {
// show popup
showDialog(
context: context,
builder: (ctx) => AlertDialog(
icon: const Icon(Icons.delete),
title: Text(
AppLocalizations.of(context)!.deleteProduct),
content: Text(AppLocalizations.of(context)!
.deleteProductConfirm(product?.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: 'deleteProduct',
target: user.server,
body: {
'room': widget.room,
'server': widget.server,
'listProdID': product?.id ?? ""
},
credentials: user),
onOK: (_) async {
// TODO: remove cached product
},
after: () {
// close popup
navInner.pop();
// close modal bottom sheet
nav.pop();
});
},
child: Text(AppLocalizations.of(context)!
.deleteProduct),
)
],
));
},
trailing: const Icon(Icons.chevron_right),
),
]
: []
])), ])),
); );
} }