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 {
|
||||
switch (type.toLowerCase()) {
|
||||
case 'love':
|
||||
return 'Friends';
|
||||
return 'Friends';
|
||||
case 'sports':
|
||||
return 'Sports';
|
||||
return 'Sports';
|
||||
case 'pets':
|
||||
return 'Pets';
|
||||
return 'Pets';
|
||||
case 'vacation':
|
||||
return 'Vacation';
|
||||
return 'Vacation';
|
||||
case 'gifts':
|
||||
return 'Gifts';
|
||||
return 'Gifts';
|
||||
case 'groceries':
|
||||
return 'Groceries';
|
||||
return 'Groceries';
|
||||
case 'fashion':
|
||||
return 'Clothing';
|
||||
return 'Clothing';
|
||||
case 'art':
|
||||
return 'Arts & Crafts';
|
||||
return 'Arts & Crafts';
|
||||
case 'tech':
|
||||
return 'Electronics';
|
||||
return 'Electronics';
|
||||
case 'home':
|
||||
return 'Home supplies';
|
||||
return 'Home supplies';
|
||||
case 'family':
|
||||
return 'Family';
|
||||
return 'Family';
|
||||
case 'social':
|
||||
return 'Social';
|
||||
return 'Social';
|
||||
case 'other':
|
||||
default:
|
||||
return 'Other';
|
||||
return 'Other';
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,44 +161,44 @@ class RoomIcon {
|
|||
String path = "";
|
||||
switch (type.toLowerCase()) {
|
||||
case 'love':
|
||||
path = 'undraw/undraw_couple.svg';
|
||||
break;
|
||||
path = 'undraw/undraw_couple.svg';
|
||||
break;
|
||||
case 'sports':
|
||||
path = 'undraw/undraw_greek_freak.svg';
|
||||
break;
|
||||
path = 'undraw/undraw_greek_freak.svg';
|
||||
break;
|
||||
case 'pets':
|
||||
path = 'undraw/undraw_dog.svg';
|
||||
break;
|
||||
path = 'undraw/undraw_dog.svg';
|
||||
break;
|
||||
case 'vacation':
|
||||
path = 'undraw/undraw_trip.svg';
|
||||
break;
|
||||
path = 'undraw/undraw_trip.svg';
|
||||
break;
|
||||
case 'gifts':
|
||||
path = 'undraw/undraw_gifts.svg';
|
||||
break;
|
||||
path = 'undraw/undraw_gifts.svg';
|
||||
break;
|
||||
case 'groceries':
|
||||
path = 'undraw/undraw_gone_shopping.svg';
|
||||
break;
|
||||
path = 'undraw/undraw_gone_shopping.svg';
|
||||
break;
|
||||
case 'fashion':
|
||||
path = 'undraw/undraw_jewelry.svg';
|
||||
break;
|
||||
path = 'undraw/undraw_jewelry.svg';
|
||||
break;
|
||||
case 'art':
|
||||
path = 'undraw/undraw_sculpting.svg';
|
||||
break;
|
||||
path = 'undraw/undraw_sculpting.svg';
|
||||
break;
|
||||
case 'tech':
|
||||
path = 'undraw/undraw_progressive_app.svg';
|
||||
break;
|
||||
path = 'undraw/undraw_progressive_app.svg';
|
||||
break;
|
||||
case 'home':
|
||||
path = 'undraw/undraw_under_construction.svg';
|
||||
break;
|
||||
path = 'undraw/undraw_under_construction.svg';
|
||||
break;
|
||||
case 'family':
|
||||
path = 'undraw/undraw_family.svg';
|
||||
break;
|
||||
path = 'undraw/undraw_family.svg';
|
||||
break;
|
||||
case 'social':
|
||||
path = 'undraw/undraw_pizza_sharing.svg';
|
||||
break;
|
||||
path = 'undraw/undraw_pizza_sharing.svg';
|
||||
break;
|
||||
case 'other':
|
||||
default:
|
||||
path = 'undraw/undraw_file_manager.svg';
|
||||
path = 'undraw/undraw_file_manager.svg';
|
||||
}
|
||||
|
||||
return asset(path);
|
||||
|
@ -214,7 +214,7 @@ class Room {
|
|||
RoomVisibility? visibility = RoomVisibility.private;
|
||||
|
||||
Room(
|
||||
{required this.id,
|
||||
{required this.id,
|
||||
required this.serverTag,
|
||||
this.name = "",
|
||||
this.description = "",
|
||||
|
@ -263,12 +263,12 @@ class Room {
|
|||
|
||||
factory Room.fromMap(Map<String, dynamic> map) {
|
||||
return Room(
|
||||
id: map['id'],
|
||||
serverTag: map['server'],
|
||||
name: map['name'],
|
||||
description: map['description'] ?? '',
|
||||
icon: RoomIcon(type: map['icon'] ?? 'Other'),
|
||||
visibility: RoomVisibility(map['visibility'] ?? 0));
|
||||
id: map['id'],
|
||||
serverTag: map['server'],
|
||||
name: map['name'],
|
||||
description: map['description'] ?? '',
|
||||
icon: RoomIcon(type: map['icon'] ?? 'Other'),
|
||||
visibility: RoomVisibility(map['visibility'] ?? 0));
|
||||
}
|
||||
|
||||
Map<String, dynamic> toMap() {
|
||||
|
@ -284,12 +284,12 @@ class Room {
|
|||
|
||||
factory Room.fromJSON(dynamic json) {
|
||||
return Room(
|
||||
id: json['name'],
|
||||
serverTag: json['server'],
|
||||
name: json['title'],
|
||||
description: json['description'],
|
||||
icon: RoomIcon(type: json['icon']),
|
||||
visibility: RoomVisibility(json['visibility']));
|
||||
id: json['name'],
|
||||
serverTag: json['server'],
|
||||
name: json['title'],
|
||||
description: json['description'],
|
||||
icon: RoomIcon(type: json['icon']),
|
||||
visibility: RoomVisibility(json['visibility']));
|
||||
}
|
||||
|
||||
Future<void> toDisk() async {
|
||||
|
@ -308,7 +308,7 @@ class Room {
|
|||
}
|
||||
|
||||
static Future<Room> fromDisk(
|
||||
{required String id, required String serverTag}) async {
|
||||
{required String id, required String serverTag}) async {
|
||||
final db = Localstore.instance;
|
||||
final raw = await db.collection('rooms').doc('$id@$serverTag').get();
|
||||
return Room.fromMap(raw!);
|
||||
|
@ -323,13 +323,13 @@ class RoomMember {
|
|||
final bool isAdmin;
|
||||
|
||||
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) {
|
||||
return RoomMember(
|
||||
id: json['name'],
|
||||
serverTag: json['server'],
|
||||
isAdmin: json['admin'] == 1);
|
||||
id: json['name'],
|
||||
serverTag: json['server'],
|
||||
isAdmin: json['admin'] == 1);
|
||||
}
|
||||
|
||||
String get humanReadableName {
|
||||
|
@ -344,17 +344,17 @@ class RoomInfo {
|
|||
final int permissions;
|
||||
|
||||
const RoomInfo(
|
||||
{required this.permissions,
|
||||
{required this.permissions,
|
||||
required this.owner,
|
||||
required this.isAdmin,
|
||||
required this.isOwner});
|
||||
|
||||
factory RoomInfo.fromJSON(dynamic json) {
|
||||
return RoomInfo(
|
||||
permissions: json['rights'],
|
||||
owner: json['owner'],
|
||||
isAdmin: json['isAdmin'],
|
||||
isOwner: json['isOwner']);
|
||||
permissions: json['rights'],
|
||||
owner: json['owner'],
|
||||
isAdmin: json['isAdmin'],
|
||||
isOwner: json['isOwner']);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -364,43 +364,102 @@ class RoomCategory {
|
|||
final ColorSwatch<int> color;
|
||||
|
||||
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) {
|
||||
return RoomCategory(
|
||||
id: json['id'],
|
||||
name: json['title'],
|
||||
color: colorFromString(json['color']));
|
||||
id: json['id'],
|
||||
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) {
|
||||
switch (text.toLowerCase()) {
|
||||
case 'red-acc':
|
||||
return Colors.redAccent;
|
||||
return Colors.redAccent;
|
||||
case 'green-acc':
|
||||
return Colors.greenAccent;
|
||||
return Colors.greenAccent;
|
||||
case 'yellow-acc':
|
||||
return Colors.yellowAccent;
|
||||
return Colors.yellowAccent;
|
||||
case 'blue-acc':
|
||||
return Colors.blueAccent;
|
||||
return Colors.blueAccent;
|
||||
case 'aqua-acc':
|
||||
return Colors.tealAccent;
|
||||
return Colors.tealAccent;
|
||||
case 'purple-acc':
|
||||
return Colors.purpleAccent;
|
||||
return Colors.purpleAccent;
|
||||
|
||||
case 'red':
|
||||
return Colors.red;
|
||||
return Colors.red;
|
||||
case 'green':
|
||||
return Colors.green;
|
||||
return Colors.green;
|
||||
case 'yellow':
|
||||
return Colors.yellow;
|
||||
return Colors.yellow;
|
||||
case 'blue':
|
||||
return Colors.blue;
|
||||
return Colors.blue;
|
||||
case 'aqua':
|
||||
return Colors.teal;
|
||||
return Colors.teal;
|
||||
case 'purple':
|
||||
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": {
|
||||
"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"
|
||||
}
|
||||
|
|
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/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';
|
||||
|
||||
|
@ -28,9 +29,9 @@ void main() {
|
|||
}
|
||||
|
||||
final GlobalKey<NavigatorState> _rootNavigatorKey =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||
GlobalKey<NavigatorState>(debugLabel: 'root');
|
||||
final GlobalKey<NavigatorState> _userShellNavigatorKey =
|
||||
GlobalKey<NavigatorState>(debugLabel: 'user');
|
||||
GlobalKey<NavigatorState>(debugLabel: 'user');
|
||||
|
||||
class OutbagApp extends StatefulWidget {
|
||||
const OutbagApp({super.key});
|
||||
|
@ -52,7 +53,7 @@ class _OutbagAppState extends State {
|
|||
try {
|
||||
final theme = await AppTheme.fromDisk();
|
||||
setState(() {
|
||||
this.theme = theme;
|
||||
this.theme = theme;
|
||||
});
|
||||
} catch (_) {}
|
||||
}
|
||||
|
@ -62,14 +63,14 @@ class _OutbagAppState extends State {
|
|||
try {
|
||||
final user = await User.fromDisk();
|
||||
setState(() {
|
||||
this.user = user;
|
||||
this.user = user;
|
||||
});
|
||||
} catch (_) {
|
||||
// user unavailable
|
||||
// invalid credentials
|
||||
// log out
|
||||
setState(() {
|
||||
user = null;
|
||||
user = null;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -79,8 +80,8 @@ class _OutbagAppState extends State {
|
|||
super.initState();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
loadTheme();
|
||||
loadUser();
|
||||
loadTheme();
|
||||
loadUser();
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -90,31 +91,31 @@ class _OutbagAppState extends State {
|
|||
// with existing details
|
||||
// NOTE: also functions as a way to verify ther data
|
||||
await doNetworkRequest(null,
|
||||
req: () => postWithCreadentials(
|
||||
target: user.server,
|
||||
path: 'getMyAccount',
|
||||
credentials: user,
|
||||
body: {}),
|
||||
onOK: (body) async {
|
||||
final i = AccountMeta.fromJSON(body['data']);
|
||||
info = i;
|
||||
},
|
||||
onServerErr: (_) {
|
||||
info = null;
|
||||
req: () => postWithCreadentials(
|
||||
target: user.server,
|
||||
path: 'getMyAccount',
|
||||
credentials: user,
|
||||
body: {}),
|
||||
onOK: (body) async {
|
||||
final i = AccountMeta.fromJSON(body['data']);
|
||||
info = i;
|
||||
},
|
||||
onServerErr: (_) {
|
||||
info = null;
|
||||
|
||||
setState(() {
|
||||
setState(() {
|
||||
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;
|
||||
}
|
||||
|
@ -122,143 +123,156 @@ class _OutbagAppState extends State {
|
|||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
Provider<AppTheme>.value(value: theme),
|
||||
providers: [
|
||||
Provider<AppTheme>.value(value: theme),
|
||||
],
|
||||
child: MaterialApp.router(
|
||||
title: "Outbag",
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
AppLocalizations.delegate
|
||||
],
|
||||
child: MaterialApp.router(
|
||||
title: "Outbag",
|
||||
localizationsDelegates: const [
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
AppLocalizations.delegate
|
||||
],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
themeMode: theme.mode,
|
||||
theme: ThemeData(useMaterial3: true, brightness: Brightness.light),
|
||||
darkTheme: ThemeData(useMaterial3: true, brightness: Brightness.dark),
|
||||
routerConfig: GoRouter(
|
||||
navigatorKey: _rootNavigatorKey,
|
||||
initialLocation: '/',
|
||||
redirect: (context, state) async {
|
||||
if (user == null) {
|
||||
// prelogin
|
||||
if (!state.subloc.startsWith('/welcome')) {
|
||||
// prevent unauthorized user from accessing home
|
||||
return '/welcome';
|
||||
}
|
||||
} else {
|
||||
// post login
|
||||
if (state.subloc.startsWith('/welcome')) {
|
||||
// prevent authorized user from accessing /welcome
|
||||
return '/';
|
||||
}
|
||||
}
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
themeMode: theme.mode,
|
||||
theme: ThemeData(useMaterial3: true, brightness: Brightness.light),
|
||||
darkTheme: ThemeData(useMaterial3: true, brightness: Brightness.dark),
|
||||
routerConfig: GoRouter(
|
||||
navigatorKey: _rootNavigatorKey,
|
||||
initialLocation: '/',
|
||||
redirect: (context, state) async {
|
||||
if (user == null) {
|
||||
// prelogin
|
||||
if (!state.subloc.startsWith('/welcome')) {
|
||||
// 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>[
|
||||
// unauthorized routes
|
||||
GoRoute(
|
||||
name: 'welcome',
|
||||
path: '/welcome',
|
||||
builder: (context, state) => const WelcomePage(),
|
||||
routes: <RouteBase>[
|
||||
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
|
||||
name: 'signin',
|
||||
path: 'signin',
|
||||
builder: (context, state) =>
|
||||
AuthPage(mode: Mode.signin, refresh: loadUser),
|
||||
),
|
||||
GoRoute(
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
builder: (context, state) => const Text('About'))
|
||||
]),
|
||||
));
|
||||
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'] ?? '')),
|
||||
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: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