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
|
@ -127,32 +127,32 @@ class RoomIcon {
|
||||||
String get text {
|
String get text {
|
||||||
switch (type.toLowerCase()) {
|
switch (type.toLowerCase()) {
|
||||||
case 'love':
|
case 'love':
|
||||||
return 'Friends';
|
return 'Friends';
|
||||||
case 'sports':
|
case 'sports':
|
||||||
return 'Sports';
|
return 'Sports';
|
||||||
case 'pets':
|
case 'pets':
|
||||||
return 'Pets';
|
return 'Pets';
|
||||||
case 'vacation':
|
case 'vacation':
|
||||||
return 'Vacation';
|
return 'Vacation';
|
||||||
case 'gifts':
|
case 'gifts':
|
||||||
return 'Gifts';
|
return 'Gifts';
|
||||||
case 'groceries':
|
case 'groceries':
|
||||||
return 'Groceries';
|
return 'Groceries';
|
||||||
case 'fashion':
|
case 'fashion':
|
||||||
return 'Clothing';
|
return 'Clothing';
|
||||||
case 'art':
|
case 'art':
|
||||||
return 'Arts & Crafts';
|
return 'Arts & Crafts';
|
||||||
case 'tech':
|
case 'tech':
|
||||||
return 'Electronics';
|
return 'Electronics';
|
||||||
case 'home':
|
case 'home':
|
||||||
return 'Home supplies';
|
return 'Home supplies';
|
||||||
case 'family':
|
case 'family':
|
||||||
return 'Family';
|
return 'Family';
|
||||||
case 'social':
|
case 'social':
|
||||||
return 'Social';
|
return 'Social';
|
||||||
case 'other':
|
case 'other':
|
||||||
default:
|
default:
|
||||||
return 'Other';
|
return 'Other';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,44 +161,44 @@ class RoomIcon {
|
||||||
String path = "";
|
String path = "";
|
||||||
switch (type.toLowerCase()) {
|
switch (type.toLowerCase()) {
|
||||||
case 'love':
|
case 'love':
|
||||||
path = 'undraw/undraw_couple.svg';
|
path = 'undraw/undraw_couple.svg';
|
||||||
break;
|
break;
|
||||||
case 'sports':
|
case 'sports':
|
||||||
path = 'undraw/undraw_greek_freak.svg';
|
path = 'undraw/undraw_greek_freak.svg';
|
||||||
break;
|
break;
|
||||||
case 'pets':
|
case 'pets':
|
||||||
path = 'undraw/undraw_dog.svg';
|
path = 'undraw/undraw_dog.svg';
|
||||||
break;
|
break;
|
||||||
case 'vacation':
|
case 'vacation':
|
||||||
path = 'undraw/undraw_trip.svg';
|
path = 'undraw/undraw_trip.svg';
|
||||||
break;
|
break;
|
||||||
case 'gifts':
|
case 'gifts':
|
||||||
path = 'undraw/undraw_gifts.svg';
|
path = 'undraw/undraw_gifts.svg';
|
||||||
break;
|
break;
|
||||||
case 'groceries':
|
case 'groceries':
|
||||||
path = 'undraw/undraw_gone_shopping.svg';
|
path = 'undraw/undraw_gone_shopping.svg';
|
||||||
break;
|
break;
|
||||||
case 'fashion':
|
case 'fashion':
|
||||||
path = 'undraw/undraw_jewelry.svg';
|
path = 'undraw/undraw_jewelry.svg';
|
||||||
break;
|
break;
|
||||||
case 'art':
|
case 'art':
|
||||||
path = 'undraw/undraw_sculpting.svg';
|
path = 'undraw/undraw_sculpting.svg';
|
||||||
break;
|
break;
|
||||||
case 'tech':
|
case 'tech':
|
||||||
path = 'undraw/undraw_progressive_app.svg';
|
path = 'undraw/undraw_progressive_app.svg';
|
||||||
break;
|
break;
|
||||||
case 'home':
|
case 'home':
|
||||||
path = 'undraw/undraw_under_construction.svg';
|
path = 'undraw/undraw_under_construction.svg';
|
||||||
break;
|
break;
|
||||||
case 'family':
|
case 'family':
|
||||||
path = 'undraw/undraw_family.svg';
|
path = 'undraw/undraw_family.svg';
|
||||||
break;
|
break;
|
||||||
case 'social':
|
case 'social':
|
||||||
path = 'undraw/undraw_pizza_sharing.svg';
|
path = 'undraw/undraw_pizza_sharing.svg';
|
||||||
break;
|
break;
|
||||||
case 'other':
|
case 'other':
|
||||||
default:
|
default:
|
||||||
path = 'undraw/undraw_file_manager.svg';
|
path = 'undraw/undraw_file_manager.svg';
|
||||||
}
|
}
|
||||||
|
|
||||||
return asset(path);
|
return asset(path);
|
||||||
|
@ -214,7 +214,7 @@ class Room {
|
||||||
RoomVisibility? visibility = RoomVisibility.private;
|
RoomVisibility? visibility = RoomVisibility.private;
|
||||||
|
|
||||||
Room(
|
Room(
|
||||||
{required this.id,
|
{required this.id,
|
||||||
required this.serverTag,
|
required this.serverTag,
|
||||||
this.name = "",
|
this.name = "",
|
||||||
this.description = "",
|
this.description = "",
|
||||||
|
@ -263,12 +263,12 @@ class Room {
|
||||||
|
|
||||||
factory Room.fromMap(Map<String, dynamic> map) {
|
factory Room.fromMap(Map<String, dynamic> map) {
|
||||||
return Room(
|
return Room(
|
||||||
id: map['id'],
|
id: map['id'],
|
||||||
serverTag: map['server'],
|
serverTag: map['server'],
|
||||||
name: map['name'],
|
name: map['name'],
|
||||||
description: map['description'] ?? '',
|
description: map['description'] ?? '',
|
||||||
icon: RoomIcon(type: map['icon'] ?? 'Other'),
|
icon: RoomIcon(type: map['icon'] ?? 'Other'),
|
||||||
visibility: RoomVisibility(map['visibility'] ?? 0));
|
visibility: RoomVisibility(map['visibility'] ?? 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toMap() {
|
Map<String, dynamic> toMap() {
|
||||||
|
@ -284,12 +284,12 @@ class Room {
|
||||||
|
|
||||||
factory Room.fromJSON(dynamic json) {
|
factory Room.fromJSON(dynamic json) {
|
||||||
return Room(
|
return Room(
|
||||||
id: json['name'],
|
id: json['name'],
|
||||||
serverTag: json['server'],
|
serverTag: json['server'],
|
||||||
name: json['title'],
|
name: json['title'],
|
||||||
description: json['description'],
|
description: json['description'],
|
||||||
icon: RoomIcon(type: json['icon']),
|
icon: RoomIcon(type: json['icon']),
|
||||||
visibility: RoomVisibility(json['visibility']));
|
visibility: RoomVisibility(json['visibility']));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> toDisk() async {
|
Future<void> toDisk() async {
|
||||||
|
@ -308,7 +308,7 @@ class Room {
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Room> fromDisk(
|
static Future<Room> fromDisk(
|
||||||
{required String id, required String serverTag}) async {
|
{required String id, required String serverTag}) async {
|
||||||
final db = Localstore.instance;
|
final db = Localstore.instance;
|
||||||
final raw = await db.collection('rooms').doc('$id@$serverTag').get();
|
final raw = await db.collection('rooms').doc('$id@$serverTag').get();
|
||||||
return Room.fromMap(raw!);
|
return Room.fromMap(raw!);
|
||||||
|
@ -323,13 +323,13 @@ class RoomMember {
|
||||||
final bool isAdmin;
|
final bool isAdmin;
|
||||||
|
|
||||||
const RoomMember(
|
const RoomMember(
|
||||||
{required this.id, required this.serverTag, required this.isAdmin});
|
{required this.id, required this.serverTag, required this.isAdmin});
|
||||||
|
|
||||||
factory RoomMember.fromJSON(dynamic json) {
|
factory RoomMember.fromJSON(dynamic json) {
|
||||||
return RoomMember(
|
return RoomMember(
|
||||||
id: json['name'],
|
id: json['name'],
|
||||||
serverTag: json['server'],
|
serverTag: json['server'],
|
||||||
isAdmin: json['admin'] == 1);
|
isAdmin: json['admin'] == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
String get humanReadableName {
|
String get humanReadableName {
|
||||||
|
@ -344,17 +344,17 @@ class RoomInfo {
|
||||||
final int permissions;
|
final int permissions;
|
||||||
|
|
||||||
const RoomInfo(
|
const RoomInfo(
|
||||||
{required this.permissions,
|
{required this.permissions,
|
||||||
required this.owner,
|
required this.owner,
|
||||||
required this.isAdmin,
|
required this.isAdmin,
|
||||||
required this.isOwner});
|
required this.isOwner});
|
||||||
|
|
||||||
factory RoomInfo.fromJSON(dynamic json) {
|
factory RoomInfo.fromJSON(dynamic json) {
|
||||||
return RoomInfo(
|
return RoomInfo(
|
||||||
permissions: json['rights'],
|
permissions: json['rights'],
|
||||||
owner: json['owner'],
|
owner: json['owner'],
|
||||||
isAdmin: json['isAdmin'],
|
isAdmin: json['isAdmin'],
|
||||||
isOwner: json['isOwner']);
|
isOwner: json['isOwner']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -364,43 +364,102 @@ class RoomCategory {
|
||||||
final ColorSwatch<int> color;
|
final ColorSwatch<int> color;
|
||||||
|
|
||||||
const RoomCategory(
|
const RoomCategory(
|
||||||
{required this.id, required this.name, required this.color});
|
{required this.id, required this.name, required this.color});
|
||||||
|
|
||||||
factory RoomCategory.fromJSON(dynamic json) {
|
factory RoomCategory.fromJSON(dynamic json) {
|
||||||
return RoomCategory(
|
return RoomCategory(
|
||||||
id: json['id'],
|
id: json['id'],
|
||||||
name: json['title'],
|
name: json['title'],
|
||||||
color: colorFromString(json['color']));
|
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) {
|
ColorSwatch<int> colorFromString(String text) {
|
||||||
switch (text.toLowerCase()) {
|
switch (text.toLowerCase()) {
|
||||||
case 'red-acc':
|
case 'red-acc':
|
||||||
return Colors.redAccent;
|
return Colors.redAccent;
|
||||||
case 'green-acc':
|
case 'green-acc':
|
||||||
return Colors.greenAccent;
|
return Colors.greenAccent;
|
||||||
case 'yellow-acc':
|
case 'yellow-acc':
|
||||||
return Colors.yellowAccent;
|
return Colors.yellowAccent;
|
||||||
case 'blue-acc':
|
case 'blue-acc':
|
||||||
return Colors.blueAccent;
|
return Colors.blueAccent;
|
||||||
case 'aqua-acc':
|
case 'aqua-acc':
|
||||||
return Colors.tealAccent;
|
return Colors.tealAccent;
|
||||||
case 'purple-acc':
|
case 'purple-acc':
|
||||||
return Colors.purpleAccent;
|
return Colors.purpleAccent;
|
||||||
|
|
||||||
case 'red':
|
case 'red':
|
||||||
return Colors.red;
|
return Colors.red;
|
||||||
case 'green':
|
case 'green':
|
||||||
return Colors.green;
|
return Colors.green;
|
||||||
case 'yellow':
|
case 'yellow':
|
||||||
return Colors.yellow;
|
return Colors.yellow;
|
||||||
case 'blue':
|
case 'blue':
|
||||||
return Colors.blue;
|
return Colors.blue;
|
||||||
case 'aqua':
|
case 'aqua':
|
||||||
return Colors.teal;
|
return Colors.teal;
|
||||||
case 'purple':
|
case 'purple':
|
||||||
default:
|
default:
|
||||||
return Colors.purple;
|
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": "Welcome to Outbag",
|
||||||
"@welcomeTitle": {
|
"@welcomeTitle": {
|
||||||
"description": "Title shown on welcome screen"
|
"description": "Title shown on welcome screen"
|
||||||
|
@ -291,7 +286,7 @@
|
||||||
"errorAccountDeletion": "Your account no longer exists",
|
"errorAccountDeletion": "Your account no longer exists",
|
||||||
|
|
||||||
"errorNoSuchAccount": "Account does not exist",
|
"errorNoSuchAccount": "Account does not exist",
|
||||||
"errorUsernameTaken": "Already with that username already exists",
|
"errorUsernameTaken": "Account with that username already exists",
|
||||||
"errorNoSuchRoom": "Room does not exist",
|
"errorNoSuchRoom": "Room does not exist",
|
||||||
"errorRoomIdTaken": "Room with that ID already exists",
|
"errorRoomIdTaken": "Room with that ID already exists",
|
||||||
|
|
||||||
|
@ -318,5 +313,28 @@
|
||||||
"errorInvalidToken": "Invalid Token",
|
"errorInvalidToken": "Invalid Token",
|
||||||
|
|
||||||
"errorServerDoesntExist": "Server does not exist",
|
"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"
|
||||||
}
|
}
|
||||||
|
|
338
lib/main.dart
338
lib/main.dart
|
@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
||||||
import 'package:outbag_app/backend/themes.dart';
|
import 'package:outbag_app/backend/themes.dart';
|
||||||
import 'package:outbag_app/backend/user.dart';
|
import 'package:outbag_app/backend/user.dart';
|
||||||
import 'package:outbag_app/backend/request.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';
|
import 'package:outbag_app/tools/fetch_wrapper.dart';
|
||||||
|
|
||||||
|
@ -28,9 +29,9 @@ void main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
final GlobalKey<NavigatorState> _rootNavigatorKey =
|
final GlobalKey<NavigatorState> _rootNavigatorKey =
|
||||||
GlobalKey<NavigatorState>(debugLabel: 'root');
|
GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||||
final GlobalKey<NavigatorState> _userShellNavigatorKey =
|
final GlobalKey<NavigatorState> _userShellNavigatorKey =
|
||||||
GlobalKey<NavigatorState>(debugLabel: 'user');
|
GlobalKey<NavigatorState>(debugLabel: 'user');
|
||||||
|
|
||||||
class OutbagApp extends StatefulWidget {
|
class OutbagApp extends StatefulWidget {
|
||||||
const OutbagApp({super.key});
|
const OutbagApp({super.key});
|
||||||
|
@ -52,7 +53,7 @@ class _OutbagAppState extends State {
|
||||||
try {
|
try {
|
||||||
final theme = await AppTheme.fromDisk();
|
final theme = await AppTheme.fromDisk();
|
||||||
setState(() {
|
setState(() {
|
||||||
this.theme = theme;
|
this.theme = theme;
|
||||||
});
|
});
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
@ -62,14 +63,14 @@ class _OutbagAppState extends State {
|
||||||
try {
|
try {
|
||||||
final user = await User.fromDisk();
|
final user = await User.fromDisk();
|
||||||
setState(() {
|
setState(() {
|
||||||
this.user = user;
|
this.user = user;
|
||||||
});
|
});
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// user unavailable
|
// user unavailable
|
||||||
// invalid credentials
|
// invalid credentials
|
||||||
// log out
|
// log out
|
||||||
setState(() {
|
setState(() {
|
||||||
user = null;
|
user = null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -79,8 +80,8 @@ class _OutbagAppState extends State {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
loadTheme();
|
loadTheme();
|
||||||
loadUser();
|
loadUser();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,31 +91,31 @@ class _OutbagAppState extends State {
|
||||||
// with existing details
|
// with existing details
|
||||||
// NOTE: also functions as a way to verify ther data
|
// NOTE: also functions as a way to verify ther data
|
||||||
await doNetworkRequest(null,
|
await doNetworkRequest(null,
|
||||||
req: () => postWithCreadentials(
|
req: () => postWithCreadentials(
|
||||||
target: user.server,
|
target: user.server,
|
||||||
path: 'getMyAccount',
|
path: 'getMyAccount',
|
||||||
credentials: user,
|
credentials: user,
|
||||||
body: {}),
|
body: {}),
|
||||||
onOK: (body) async {
|
onOK: (body) async {
|
||||||
final i = AccountMeta.fromJSON(body['data']);
|
final i = AccountMeta.fromJSON(body['data']);
|
||||||
info = i;
|
info = i;
|
||||||
},
|
},
|
||||||
onServerErr: (_) {
|
onServerErr: (_) {
|
||||||
info = null;
|
info = null;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
this.user = null;
|
this.user = null;
|
||||||
});
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
onNetworkErr: () {
|
|
||||||
info = null;
|
|
||||||
// user is currently offline
|
|
||||||
// approve login,
|
|
||||||
// until user goes back offline
|
|
||||||
// NOTE TODO: check user data once online
|
|
||||||
return true;
|
|
||||||
});
|
});
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
onNetworkErr: () {
|
||||||
|
info = null;
|
||||||
|
// user is currently offline
|
||||||
|
// approve login,
|
||||||
|
// until user goes back offline
|
||||||
|
// NOTE TODO: check user data once online
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
@ -122,143 +123,156 @@ class _OutbagAppState extends State {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return MultiProvider(
|
return MultiProvider(
|
||||||
providers: [
|
providers: [
|
||||||
Provider<AppTheme>.value(value: theme),
|
Provider<AppTheme>.value(value: theme),
|
||||||
|
],
|
||||||
|
child: MaterialApp.router(
|
||||||
|
title: "Outbag",
|
||||||
|
localizationsDelegates: const [
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
AppLocalizations.delegate
|
||||||
],
|
],
|
||||||
child: MaterialApp.router(
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
title: "Outbag",
|
themeMode: theme.mode,
|
||||||
localizationsDelegates: const [
|
theme: ThemeData(useMaterial3: true, brightness: Brightness.light),
|
||||||
GlobalMaterialLocalizations.delegate,
|
darkTheme: ThemeData(useMaterial3: true, brightness: Brightness.dark),
|
||||||
GlobalWidgetsLocalizations.delegate,
|
routerConfig: GoRouter(
|
||||||
GlobalCupertinoLocalizations.delegate,
|
navigatorKey: _rootNavigatorKey,
|
||||||
AppLocalizations.delegate
|
initialLocation: '/',
|
||||||
],
|
redirect: (context, state) async {
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
if (user == null) {
|
||||||
themeMode: theme.mode,
|
// prelogin
|
||||||
theme: ThemeData(useMaterial3: true, brightness: Brightness.light),
|
if (!state.subloc.startsWith('/welcome')) {
|
||||||
darkTheme: ThemeData(useMaterial3: true, brightness: Brightness.dark),
|
// prevent unauthorized user from accessing home
|
||||||
routerConfig: GoRouter(
|
return '/welcome';
|
||||||
navigatorKey: _rootNavigatorKey,
|
}
|
||||||
initialLocation: '/',
|
} else {
|
||||||
redirect: (context, state) async {
|
// post login
|
||||||
if (user == null) {
|
if (state.subloc.startsWith('/welcome')) {
|
||||||
// prelogin
|
// prevent authorized user from accessing /welcome
|
||||||
if (!state.subloc.startsWith('/welcome')) {
|
return '/';
|
||||||
// prevent unauthorized user from accessing home
|
}
|
||||||
return '/welcome';
|
}
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// post login
|
|
||||||
if (state.subloc.startsWith('/welcome')) {
|
|
||||||
// prevent authorized user from accessing /welcome
|
|
||||||
return '/';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
routes: <RouteBase>[
|
||||||
|
// unauthorized routes
|
||||||
|
GoRoute(
|
||||||
|
name: 'welcome',
|
||||||
|
path: '/welcome',
|
||||||
|
builder: (context, state) => const WelcomePage(),
|
||||||
routes: <RouteBase>[
|
routes: <RouteBase>[
|
||||||
// unauthorized routes
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
name: 'welcome',
|
name: 'signin',
|
||||||
path: '/welcome',
|
path: 'signin',
|
||||||
builder: (context, state) => const WelcomePage(),
|
builder: (context, state) =>
|
||||||
routes: <RouteBase>[
|
AuthPage(mode: Mode.signin, refresh: loadUser),
|
||||||
GoRoute(
|
),
|
||||||
name: 'signin',
|
|
||||||
path: 'signin',
|
|
||||||
builder: (context, state) =>
|
|
||||||
AuthPage(mode: Mode.signin, refresh: loadUser),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'signup',
|
|
||||||
path: 'signup',
|
|
||||||
builder: (context, state) =>
|
|
||||||
AuthPage(mode: Mode.signup, refresh: loadUser),
|
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
name: 'signup-ota',
|
|
||||||
path: 'signup-ota',
|
|
||||||
builder: (context, state) =>
|
|
||||||
AuthPage(mode: Mode.signupOTA, refresh: loadUser),
|
|
||||||
),
|
|
||||||
]),
|
|
||||||
|
|
||||||
// authorized routes
|
|
||||||
ShellRoute(
|
|
||||||
navigatorKey: _userShellNavigatorKey,
|
|
||||||
builder: (context, state, child) => Provider.value(
|
|
||||||
value: user!,
|
|
||||||
child: FutureProvider(
|
|
||||||
initialData: null,
|
|
||||||
child: child,
|
|
||||||
create: (context)=>fetchInfo(context.read<User>()),
|
|
||||||
)),
|
|
||||||
routes: <RouteBase>[
|
|
||||||
GoRoute(
|
|
||||||
path: '/',
|
|
||||||
name: 'home',
|
|
||||||
builder: (context, state) => const HomePage(),
|
|
||||||
routes: [
|
|
||||||
GoRoute(
|
|
||||||
name: 'settings',
|
|
||||||
path: 'settings',
|
|
||||||
builder: (context, state) =>
|
|
||||||
SettingsPage(
|
|
||||||
refreshTheme: loadTheme,
|
|
||||||
refreshUser: loadUser
|
|
||||||
)),
|
|
||||||
GoRoute(
|
|
||||||
path: 'join-room',
|
|
||||||
name: 'add-room',
|
|
||||||
builder: (context, state) =>
|
|
||||||
const JoinRoomPage(),
|
|
||||||
routes: <RouteBase>[
|
|
||||||
GoRoute(
|
|
||||||
path: 'new',
|
|
||||||
name: 'new-room',
|
|
||||||
builder: (context, state) =>
|
|
||||||
const NewRoomPage()),
|
|
||||||
]),
|
|
||||||
GoRoute(
|
|
||||||
name: 'room',
|
|
||||||
path: 'r/:server/:id',
|
|
||||||
builder: (context, state) => RoomPage(
|
|
||||||
state.params['server'] ?? '',
|
|
||||||
state.params['id'] ?? ''),
|
|
||||||
routes: <RouteBase>[
|
|
||||||
GoRoute(
|
|
||||||
name: 'edit-room',
|
|
||||||
path: 'edit',
|
|
||||||
builder: (context, state) => EditRoomPage(
|
|
||||||
state.params['server'] ?? '',
|
|
||||||
state.params['id'] ?? '')),
|
|
||||||
GoRoute(
|
|
||||||
name: 'room-members',
|
|
||||||
path: 'members',
|
|
||||||
builder: (context, state) =>
|
|
||||||
ManageRoomMembersPage(
|
|
||||||
state.params['server'] ?? '',
|
|
||||||
state.params['id'] ?? '')),
|
|
||||||
GoRoute(
|
|
||||||
name: 'room-permissions',
|
|
||||||
path: 'roles',
|
|
||||||
builder: (context, state) =>
|
|
||||||
EditRoomPermissionSetPage(
|
|
||||||
state.params['server'] ?? '',
|
|
||||||
state.params['id'] ?? '')),
|
|
||||||
])
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
|
|
||||||
// routes that can be accessed
|
|
||||||
// with and without an account
|
|
||||||
// i.e the about screen
|
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/about',
|
name: 'signup',
|
||||||
name: 'about',
|
path: 'signup',
|
||||||
builder: (context, state) => const Text('About'))
|
builder: (context, state) =>
|
||||||
]),
|
AuthPage(mode: Mode.signup, refresh: loadUser),
|
||||||
));
|
),
|
||||||
|
GoRoute(
|
||||||
|
name: 'signup-ota',
|
||||||
|
path: 'signup-ota',
|
||||||
|
builder: (context, state) =>
|
||||||
|
AuthPage(mode: Mode.signupOTA, refresh: loadUser),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
|
||||||
|
// authorized routes
|
||||||
|
ShellRoute(
|
||||||
|
navigatorKey: _userShellNavigatorKey,
|
||||||
|
builder: (context, state, child) => Provider.value(
|
||||||
|
value: user!,
|
||||||
|
child: FutureProvider(
|
||||||
|
initialData: null,
|
||||||
|
child: child,
|
||||||
|
create: (context)=>fetchInfo(context.read<User>()),
|
||||||
|
)),
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
builder: (context, state) => const HomePage(),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
name: 'settings',
|
||||||
|
path: 'settings',
|
||||||
|
builder: (context, state) =>
|
||||||
|
SettingsPage(
|
||||||
|
refreshTheme: loadTheme,
|
||||||
|
refreshUser: loadUser
|
||||||
|
)),
|
||||||
|
GoRoute(
|
||||||
|
path: 'join-room',
|
||||||
|
name: 'add-room',
|
||||||
|
builder: (context, state) =>
|
||||||
|
const JoinRoomPage(),
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
path: 'new',
|
||||||
|
name: 'new-room',
|
||||||
|
builder: (context, state) =>
|
||||||
|
const NewRoomPage()),
|
||||||
|
]),
|
||||||
|
GoRoute(
|
||||||
|
name: 'room',
|
||||||
|
path: 'r/:server/:id',
|
||||||
|
builder: (context, state) => RoomPage(
|
||||||
|
state.params['server'] ?? '',
|
||||||
|
state.params['id'] ?? ''),
|
||||||
|
routes: <RouteBase>[
|
||||||
|
GoRoute(
|
||||||
|
name: 'edit-room',
|
||||||
|
path: 'edit',
|
||||||
|
builder: (context, state) => EditRoomPage(
|
||||||
|
state.params['server'] ?? '',
|
||||||
|
state.params['id'] ?? '')),
|
||||||
|
GoRoute(
|
||||||
|
name: 'room-members',
|
||||||
|
path: 'members',
|
||||||
|
builder: (context, state) =>
|
||||||
|
ManageRoomMembersPage(
|
||||||
|
state.params['server'] ?? '',
|
||||||
|
state.params['id'] ?? '')),
|
||||||
|
GoRoute(
|
||||||
|
name: 'room-permissions',
|
||||||
|
path: 'roles',
|
||||||
|
builder: (context, 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'] ?? ''))),
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
|
||||||
|
// routes that can be accessed
|
||||||
|
// with and without an account
|
||||||
|
// i.e the about screen
|
||||||
|
GoRoute(
|
||||||
|
path: '/about',
|
||||||
|
name: 'about',
|
||||||
|
builder: (context, state) => const Text('About'))
|
||||||
|
]),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
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: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/request.dart';
|
||||||
import 'package:outbag_app/backend/room.dart';
|
import 'package:outbag_app/backend/room.dart';
|
||||||
import 'package:outbag_app/backend/user.dart';
|
import 'package:outbag_app/backend/user.dart';
|
||||||
|
@ -51,33 +53,202 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final textTheme = Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: ReorderableListView.builder(
|
body: ReorderableListView.builder(
|
||||||
|
buildDefaultDragHandles: false,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = list[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) ||
|
||||||
|
(widget.info?.isOwner ?? false) ||
|
||||||
|
((widget.info?.permissions)! &
|
||||||
|
RoomPermission.editRoomContent !=
|
||||||
|
0))
|
||||||
|
? ReorderableDragStartListener(
|
||||||
|
index: index,
|
||||||
|
child: const Icon(Icons.drag_handle)
|
||||||
|
)
|
||||||
|
: null,
|
||||||
title: Text(item.name),
|
title: Text(item.name),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// TODO show edit category popup
|
// TODO show edit category popup
|
||||||
// NOTE: maybe use ModalBottomSheet
|
// NOTE: maybe use ModalBottomSheet
|
||||||
// and show delete button in there
|
// 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,
|
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),
|
icon: const Icon(Icons.add),
|
||||||
label: Text(AppLocalizations.of(context)!.newCategoryShort),
|
label: Text(AppLocalizations.of(context)!.newCategoryShort),
|
||||||
tooltip: AppLocalizations.of(context)!.newCategoryLong,
|
tooltip: AppLocalizations.of(context)!.newCategoryLong,
|
||||||
onPressed: () {
|
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