Formatted files using dart format

This commit is contained in:
Jakob Meier 2023-12-22 20:14:36 +01:00
parent 1975d66419
commit b320d51fa1
No known key found for this signature in database
GPG key ID: 66BDC7E6A01A6152
28 changed files with 2466 additions and 2422 deletions

View file

@ -107,19 +107,19 @@ class RoomPermission {
switch (permission) {
case 1:
return trans!.roomPermissionAddItems;
return trans!.roomPermissionAddItems;
case 2:
return trans!.roomPermissionRemoveItems;
return trans!.roomPermissionRemoveItems;
case 4:
return trans!.roomPermissionEditContent;
return trans!.roomPermissionEditContent;
case 8:
return trans!.roomPermissionChangeMeta;
return trans!.roomPermissionChangeMeta;
case 16:
return trans!.roomPermissionManageOTA;
return trans!.roomPermissionManageOTA;
case 32:
return trans!.roomPermissionManageAdmins;
return trans!.roomPermissionManageAdmins;
case 64:
return trans!.roomPermissionManageMembers;
return trans!.roomPermissionManageMembers;
}
return trans!.roomPermissionUnknown;
@ -130,19 +130,19 @@ class RoomPermission {
switch (permission) {
case 1:
return trans!.roomPermissionAddItemsSubtitle;
return trans!.roomPermissionAddItemsSubtitle;
case 2:
return trans!.roomPermissionRemoveItemsSubtitle;
return trans!.roomPermissionRemoveItemsSubtitle;
case 4:
return trans!.roomPermissionEditContentSubtitle;
return trans!.roomPermissionEditContentSubtitle;
case 8:
return trans!.roomPermissionChangeMetaSubtitle;
return trans!.roomPermissionChangeMetaSubtitle;
case 16:
return trans!.roomPermissionManageOTASubtitle;
return trans!.roomPermissionManageOTASubtitle;
case 32:
return trans!.roomPermissionManageAdminsSubtitle;
return trans!.roomPermissionManageAdminsSubtitle;
case 64:
return trans!.roomPermissionManageMembersSubtitle;
return trans!.roomPermissionManageMembersSubtitle;
}
return trans!.roomPermissionUnknownSubtitle;

View file

@ -13,19 +13,19 @@ class Response {
}
Future<Response> usePostApi(
{required OutbagServer target,
{required OutbagServer target,
String path = '',
required Map<String, String> headers,
required Map<String, dynamic> body}) async {
final resp = await http.post(Uri.parse('${target.base}api/$path'),
headers: headers, body: jsonEncode({'data': body}));
headers: headers, body: jsonEncode({'data': body}));
final json = jsonDecode(resp.body);
return Response(
body: json, res: resp.statusCode == 200 ? Result.ok : Result.err);
body: json, res: resp.statusCode == 200 ? Result.ok : Result.err);
}
Future<Response> postWithCreadentials(
{required OutbagServer target,
{required OutbagServer target,
String path = '',
required Map<String, dynamic> body,
required User credentials}) async {
@ -33,14 +33,14 @@ Future<Response> postWithCreadentials(
"Content-Type": "application/json",
'Connection': 'keep-alive',
'Authorization':
'Digest name=${credentials.username} server=${target.tag} accountKey=${credentials.password}'
'Digest name=${credentials.username} server=${target.tag} accountKey=${credentials.password}'
};
return await usePostApi(
target: target, path: path, headers: headers, body: body);
target: target, path: path, headers: headers, body: body);
}
Future<Response> postWithToken(
{required OutbagServer target,
{required OutbagServer target,
String path = '',
required Map<String, dynamic> body,
required String token}) async {
@ -49,16 +49,16 @@ Future<Response> postWithToken(
'Authorization': 'Bearer $token'
};
return await usePostApi(
target: target, path: path, headers: headers, body: body);
target: target, path: path, headers: headers, body: body);
}
Future<Response> postUnauthorized(
{required OutbagServer target,
{required OutbagServer target,
String path = '',
required Map<String, dynamic> body}) async {
Map<String, String> headers = {
"Content-Type": "application/json",
};
return await usePostApi(
target: target, path: path, headers: headers, body: body);
target: target, path: path, headers: headers, body: body);
}

View file

@ -61,11 +61,10 @@ class OutbagServer {
final SharedPreferences prefs = await SharedPreferences.getInstance();
return OutbagServer(
path: prefs.getString('server-path')!,
port: prefs.getInt('server-port')!,
tag: prefs.getString('server-tag')!,
host: prefs.getString('server-host')!
);
path: prefs.getString('server-path')!,
port: prefs.getInt('server-port')!,
tag: prefs.getString('server-tag')!,
host: prefs.getString('server-host')!);
}
static Future<void> removeDisk() async {

View file

@ -322,11 +322,17 @@ class RoomMember {
final bool isInvitePending;
const RoomMember(
{required this.id, required this.serverTag, required this.isAdmin, this.isInvitePending=false});
{required this.id,
required this.serverTag,
required this.isAdmin,
this.isInvitePending = false});
factory RoomMember.fromJSON(dynamic json) {
return RoomMember(
id: json['name'], serverTag: json['server'], isAdmin: json['admin'], isInvitePending: json['confirmed']);
id: json['name'],
serverTag: json['server'],
isAdmin: json['admin'],
isInvitePending: json['confirmed']);
}
String get humanReadableName {

View file

@ -9,7 +9,8 @@ class CategoryChip extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ActionChip(
avatar: Icon(Icons.square_rounded, color: category?.color ?? RoomCategory.other(context).color),
avatar: Icon(Icons.square_rounded,
color: category?.color ?? RoomCategory.other(context).color),
label: Text(category?.name ?? RoomCategory.other(context).name),
);
}

View file

@ -12,7 +12,7 @@ class CategoryPicker extends StatelessWidget {
final String? label;
const CategoryPicker(
{super.key,
{super.key,
required this.categories,
this.selected,
this.onSelect,
@ -23,36 +23,34 @@ class CategoryPicker extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: DropdownButtonFormField<int?>(
hint: hint==null?null:Text(hint!),
decoration: InputDecoration(
label: label==null?null:Text(label!),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.category)
),
value: selected,
items: [
...categories,
RoomCategory.other(context)
].map((category)=>DropdownMenuItem<int?>(
value: category.id,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(category.name),
Icon(Icons.square_rounded,
color:category.color,
size: 32)
]
),
)).toList(),
onChanged: enabled?(cid) {
if (onSelect != null) {
onSelect!(cid);
}
}:null,
));
padding: const EdgeInsets.all(8),
child: DropdownButtonFormField<int?>(
hint: hint == null ? null : Text(hint!),
decoration: InputDecoration(
label: label == null ? null : Text(label!),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.category)),
value: selected,
items: [...categories, RoomCategory.other(context)]
.map((category) => DropdownMenuItem<int?>(
value: category.id,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(category.name),
Icon(Icons.square_rounded,
color: category.color, size: 32)
]),
))
.toList(),
onChanged: enabled
? (cid) {
if (onSelect != null) {
onSelect!(cid);
}
}
: null,
));
}
}

View file

@ -6,14 +6,10 @@ class LabeledDivider extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: [
const Expanded(child: Divider()),
Padding(
padding: const EdgeInsets.all(8),
child: Text(label)),
const Expanded(child: Divider()),
]
);
return Row(children: [
const Expanded(child: Divider()),
Padding(padding: const EdgeInsets.all(8), child: Text(label)),
const Expanded(child: Divider()),
]);
}
}

View file

@ -14,7 +14,7 @@ class ProductPicker extends StatelessWidget {
final String? help;
const ProductPicker(
{super.key,
{super.key,
required this.products,
this.selected,
this.onSelect,
@ -26,32 +26,32 @@ class ProductPicker extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: DropdownButtonFormField<int?>(
hint: hint == null ? null : Text(hint!),
decoration: InputDecoration(
label: label == null ? null : Text(label!),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.inventory_2),
helperText: help),
value: selected,
items: [
// "no product" entry
DropdownMenuItem<int?>(
value: null,
child: Text(AppLocalizations.of(context)!.productNameNone),
),
// other products
...products.map((product) => DropdownMenuItem<int?>(
value: product.id, child: Text(product.name)))
],
onChanged: enabled
? (pid) {
if (onSelect != null) {
onSelect!(pid);
}
}
: null,
));
padding: const EdgeInsets.all(8),
child: DropdownButtonFormField<int?>(
hint: hint == null ? null : Text(hint!),
decoration: InputDecoration(
label: label == null ? null : Text(label!),
border: const OutlineInputBorder(),
prefixIcon: const Icon(Icons.inventory_2),
helperText: help),
value: selected,
items: [
// "no product" entry
DropdownMenuItem<int?>(
value: null,
child: Text(AppLocalizations.of(context)!.productNameNone),
),
// other products
...products.map((product) => DropdownMenuItem<int?>(
value: product.id, child: Text(product.name)))
],
onChanged: enabled
? (pid) {
if (onSelect != null) {
onSelect!(pid);
}
}
: null,
));
}
}

View file

@ -39,12 +39,7 @@ class RoomIconPicker extends StatelessWidget {
if (onSelect != null) {
onSelect!(icon);
}
}
)
);
}).toList()
)
)
);
}));
}).toList())));
}
}

View file

@ -25,15 +25,16 @@ import 'package:outbag_app/screens/settings/main.dart';
import 'package:provider/provider.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(const OutbagApp());
}
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});
@ -55,7 +56,7 @@ class _OutbagAppState extends State {
try {
final theme = await AppTheme.fromDisk();
setState(() {
this.theme = theme;
this.theme = theme;
});
} catch (_) {}
}
@ -65,14 +66,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;
});
}
}
@ -82,8 +83,8 @@ class _OutbagAppState extends State {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) async {
loadTheme();
loadUser();
loadTheme();
loadUser();
});
}
@ -93,31 +94,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;
}
@ -125,195 +126,213 @@ class _OutbagAppState extends State {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
Provider<AppTheme>.value(value: theme),
],
child: MaterialApp.router(
title: "Outbag",
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
AppLocalizations.delegate
providers: [
Provider<AppTheme>.value(value: theme),
],
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 '/';
}
}
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 '/';
}
}
return null;
},
routes: <RouteBase>[
// unauthorized routes
GoRoute(
name: 'welcome',
path: '/welcome',
builder: (context, state) => const WelcomePage(),
return null;
},
routes: <RouteBase>[
// unauthorized routes
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) => NewRoomPage()),
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),
),
]),
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) => NewRoomPage(
server: state.params['server'] ?? '',
tag: 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/:category',
builder: (context, state)=>EditCategoryPage(
state.params['server'] ?? '',
state.params['id'] ?? '',
id: int.tryParse(state.params['category'] ?? ''))),
GoRoute(
name: 'new-product',
path: 'new-product',
builder: (context, state)=>EditProductPage(
server: state.params['server'] ?? '',
room: state.params['id'] ?? '',)),
GoRoute(
name: 'view-product',
path: 'p/:product',
builder: (context, state)=>ViewProductPage(
server: state.params['server'] ?? '',
room: state.params['id'] ?? '',
product: int.tryParse(state.params['product'] ?? '') ?? 0),
// 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: 'edit-product',
path: 'edit',
builder: (context, state)=>EditProductPage(
server: state.params['server'] ?? '',
room: state.params['id'] ?? '',
product: int.tryParse(state.params['product'] ?? ''))),
]
),
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) =>
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) => NewRoomPage(
server: state.params['server'] ?? '',
tag: 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/:category',
builder: (context, state) =>
EditCategoryPage(
state.params['server'] ?? '',
state.params['id'] ?? '',
id: int.tryParse(
state.params['category'] ??
''))),
GoRoute(
name: 'new-product',
path: 'new-product',
builder: (context, state) =>
EditProductPage(
server:
state.params['server'] ?? '',
room: state.params['id'] ?? '',
)),
GoRoute(
name: 'view-product',
path: 'p/:product',
builder: (context, state) =>
ViewProductPage(
server:
state.params['server'] ?? '',
room: state.params['id'] ?? '',
product: int.tryParse(
state.params['product'] ??
'') ??
0),
routes: [
GoRoute(
name: 'edit-product',
path: 'edit',
builder: (context, state) =>
EditProductPage(
server: state
.params['server'] ??
'',
room: state.params['id'] ??
'',
product: int.tryParse(
state.params[
'product'] ??
''))),
]),
GoRoute(
name: 'new-item',
path: 'new-item',
builder: (context, state) => EditItemPage(
server:
state.params['server'] ?? '',
room: state.params['id'] ?? '',
)),
GoRoute(
name: 'edit-item',
path: 'i/:item',
builder: (context, state) => EditItemPage(
server: state.params['server'] ?? '',
room: state.params['id'] ?? '',
item: int.tryParse(
state.params['item'] ?? '') ??
0),
)
])
]),
]),
GoRoute(
name: 'new-item',
path: 'new-item',
builder: (context, state)=>EditItemPage(
server: state.params['server'] ?? '',
room: state.params['id'] ?? '',)),
GoRoute(
name: 'edit-item',
path: 'i/:item',
builder: (context, state)=>EditItemPage(
server: state.params['server'] ?? '',
room: state.params['id'] ?? '',
item: int.tryParse(state.params['item'] ?? '') ?? 0),
)
])
]),
]),
// 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'))
]),
));
// 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'))
]),
));
}
}

View file

@ -24,12 +24,12 @@ class _HomePageState extends State<HomePage> {
// wait for background room changes
Room.listen((_) async {
try {
final newRooms = await Room.listRooms();
setState(() {
rooms = newRooms;
});
} catch (_) {}
try {
final newRooms = await Room.listRooms();
setState(() {
rooms = newRooms;
});
} catch (_) {}
});
WidgetsBinding.instance.addPostFrameCallback((_) => fetchList());
@ -43,17 +43,17 @@ class _HomePageState extends State<HomePage> {
try {
final newRooms = await Room.listRooms();
setState(() {
rooms = newRooms;
rooms = newRooms;
});
} catch (_) {}
doNetworkRequest(
sm,
req: () => postWithCreadentials(
path: 'listRooms', credentials: user, target: user.server, body: {}),
path: 'listRooms', credentials: user, target: user.server, body: {}),
onOK: (body) async {
final List<Room> list = body['data'].map<Room>((json) {
return Room.fromJSON(json);
return Room.fromJSON(json);
}).toList();
for (Room r in list) {
await r.toDisk();
@ -91,33 +91,34 @@ class _HomePageState extends State<HomePage> {
},
menuChildren: [
MenuItemButton(
leadingIcon: const Icon(Icons.settings),
child: Text(AppLocalizations.of(context)!.settings),
onPressed: () {
// show settings screen
context.goNamed('settings');
}),
...(context.watch<AccountMeta?>() != null &&
(context.watch<AccountMeta?>()?.permissions)! &
ServerPermission.allManagement !=
0)
? [
MenuItemButton(
leadingIcon: const Icon(Icons.dns),
child: Text(AppLocalizations.of(context)!.serverDashboard),
leadingIcon: const Icon(Icons.settings),
child: Text(AppLocalizations.of(context)!.settings),
onPressed: () {
// show settings screen
context.goNamed('dash');
}),
]
: [],
context.goNamed('settings');
}),
...(context.watch<AccountMeta?>() != null &&
(context.watch<AccountMeta?>()?.permissions)! &
ServerPermission.allManagement !=
0)
? [
MenuItemButton(
leadingIcon: const Icon(Icons.dns),
child: Text(
AppLocalizations.of(context)!.serverDashboard),
onPressed: () {
// show settings screen
context.goNamed('dash');
}),
]
: [],
MenuItemButton(
leadingIcon: const Icon(Icons.info_rounded),
child: Text(AppLocalizations.of(context)!.about),
onPressed: () {
// show about screen
context.goNamed('about');
}),
leadingIcon: const Icon(Icons.info_rounded),
child: Text(AppLocalizations.of(context)!.about),
onPressed: () {
// show about screen
context.goNamed('about');
}),
],
)
],
@ -127,31 +128,31 @@ class _HomePageState extends State<HomePage> {
itemBuilder: (ctx, i) {
final room = rooms[i];
return Card(
margin: const EdgeInsets.all(8.0),
clipBehavior: Clip.antiAliasWithSaveLayer,
semanticContainer: true,
child: InkWell(
onTap: () {
// open room
context.goNamed('room',
params: {'server': room.serverTag, 'id': room.id});
},
onLongPress: () {
// open bottom sheet
// NOTE: feature yet to be confirmed
},
child: Container(
padding: const EdgeInsets.fromLTRB(10, 5, 5, 10),
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,
))));
margin: const EdgeInsets.all(8.0),
clipBehavior: Clip.antiAliasWithSaveLayer,
semanticContainer: true,
child: InkWell(
onTap: () {
// open room
context.goNamed('room',
params: {'server': room.serverTag, 'id': room.id});
},
onLongPress: () {
// open bottom sheet
// NOTE: feature yet to be confirmed
},
child: Container(
padding: const EdgeInsets.fromLTRB(10, 5, 5, 10),
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(

View file

@ -0,0 +1 @@

View file

@ -30,10 +30,10 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
doNetworkRequest(
sm,
req: () => postWithCreadentials(
path: 'getRoomInfo',
credentials: user,
target: user.server,
body: {'room': widget.tag, 'server': widget.server}),
path: 'getRoomInfo',
credentials: user,
target: user.server,
body: {'room': widget.tag, 'server': widget.server}),
onAnyErr: () {
// user should not be here
// close screen
@ -43,7 +43,7 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
onOK: (body) async {
final info = RoomInfo.fromJSON(body['data']);
setState(() {
this.info = info;
this.info = info;
});
return true;
},
@ -56,26 +56,26 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
final user = context.read<User>();
doNetworkRequest(sm,
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getRoomMembers',
body: {'room': widget.tag, 'server': widget.server}),
onAnyErr: () {
// user should not be here
// close screen
router.pushReplacementNamed('home');
return false;
},
onOK: (body) {
final List<RoomMember> list = body['data'].map<RoomMember>((json) {
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getRoomMembers',
body: {'room': widget.tag, 'server': widget.server}),
onAnyErr: () {
// user should not be here
// close screen
router.pushReplacementNamed('home');
return false;
},
onOK: (body) {
final List<RoomMember> list = body['data'].map<RoomMember>((json) {
return RoomMember.fromJSON(json);
}).toList();
}).toList();
setState(() {
setState(() {
this.list = list;
});
});
});
}
@override
@ -83,21 +83,21 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
fetchUserInfo();
fetchMembers();
fetchUserInfo();
fetchMembers();
});
}
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context)
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
return Scaffold(
appBar: AppBar(
title:
Text(AppLocalizations.of(context)!.roomMembersTitle(list.length)),
Text(AppLocalizations.of(context)!.roomMembersTitle(list.length)),
//actions: [
// // NOTE: Maybe add a search icon
// // and general search functionality here
@ -109,8 +109,8 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
String role = AppLocalizations.of(context)!.roleMember;
if (info != null &&
(info?.owner)! == item.id &&
widget.server == item.serverTag) {
(info?.owner)! == item.id &&
widget.server == item.serverTag) {
role = AppLocalizations.of(context)!.roleOwner;
} else if (item.isAdmin) {
role = AppLocalizations.of(context)!.roleAdmin;
@ -119,17 +119,17 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
bool enable = true;
// perform permission check
if (info == null ||
!((info?.isAdmin)! ||
(info?.isOwner)! ||
((info?.permissions)! & oB("1100000") != 0))) {
!((info?.isAdmin)! ||
(info?.isOwner)! ||
((info?.permissions)! & oB("1100000") != 0))) {
// NOTE: do not show error message
// user should assume,
// that it wasn't even possible
// to click on ListTile
enable = false;
} else if (info != null &&
item.id == info?.owner &&
widget.server == item.serverTag) {
item.id == info?.owner &&
widget.server == item.serverTag) {
// cannot kick admin
enable = false;
}
@ -139,236 +139,236 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
subtitle: Text(role),
leading: const Icon(Icons.person),
onTap: !enable
? null
: () {
showModalBottomSheet(
context: context,
builder: (context) => BottomSheet(
onClosing: () {},
builder: (context) => Column(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: Text(item.humanReadableName,
style: textTheme.displaySmall)),
Padding(
padding: const EdgeInsets.all(8),
child: Column(
children: [
...((info?.isAdmin)! ||
(info?.isOwner)! ||
((info?.permissions)! &
RoomPermission
.changeAdmin !=
0))
? [
ListTile(
leading: const Icon(
Icons.supervisor_account),
title: Text(item.isAdmin
? AppLocalizations.of(
context)!
.removeAdminTitle
: AppLocalizations.of(
context)!
.makeAdminTitle),
subtitle: Text(item.isAdmin
? AppLocalizations.of(
context)!
.removeAdminSubtitle
: AppLocalizations.of(
context)!
.makeAdminSubtitle),
onTap: () {
// make user admin
showDialog(
context: context,
builder:
(ctx) =>
AlertDialog(
icon: const Icon(
Icons
.supervisor_account),
title: Text(item.isAdmin
? AppLocalizations.of(
context)!
.removeAdminTitle
: AppLocalizations.of(
context)!
.makeAdminTitle),
content: Text(item
.isAdmin
? AppLocalizations.of(
context)!
.removeAdminConfirm(item
.humanReadableName)
: AppLocalizations.of(
context)!
.makeAdminConfirm(
item.humanReadableName)),
actions: [
TextButton(
onPressed:
() {
// close popup
// NOTE: cancel only closes the dialog
// whilst OK closes both
Navigator.of(ctx)
.pop();
},
child: Text(
AppLocalizations.of(context)!
.cancel),
),
FilledButton(
onPressed:
() async {
// send request
final scaffMgr =
ScaffoldMessenger.of(context);
final nav =
Navigator.of(ctx);
final nav2 =
Navigator.of(context);
final user =
context.read<User>();
? null
: () {
showModalBottomSheet(
context: context,
builder: (context) => BottomSheet(
onClosing: () {},
builder: (context) => Column(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: Text(item.humanReadableName,
style: textTheme.displaySmall)),
Padding(
padding: const EdgeInsets.all(8),
child: Column(
children: [
...((info?.isAdmin)! ||
(info?.isOwner)! ||
((info?.permissions)! &
RoomPermission
.changeAdmin !=
0))
? [
ListTile(
leading: const Icon(
Icons.supervisor_account),
title: Text(item.isAdmin
? AppLocalizations.of(
context)!
.removeAdminTitle
: AppLocalizations.of(
context)!
.makeAdminTitle),
subtitle: Text(item.isAdmin
? AppLocalizations.of(
context)!
.removeAdminSubtitle
: AppLocalizations.of(
context)!
.makeAdminSubtitle),
onTap: () {
// make user admin
showDialog(
context: context,
builder:
(ctx) =>
AlertDialog(
icon: const Icon(
Icons
.supervisor_account),
title: Text(item.isAdmin
? AppLocalizations.of(
context)!
.removeAdminTitle
: AppLocalizations.of(
context)!
.makeAdminTitle),
content: Text(item
.isAdmin
? AppLocalizations.of(
context)!
.removeAdminConfirm(item
.humanReadableName)
: AppLocalizations.of(
context)!
.makeAdminConfirm(
item.humanReadableName)),
actions: [
TextButton(
onPressed:
() {
// close popup
// NOTE: cancel only closes the dialog
// whilst OK closes both
Navigator.of(ctx)
.pop();
},
child: Text(
AppLocalizations.of(context)!
.cancel),
),
FilledButton(
onPressed:
() async {
// send request
final scaffMgr =
ScaffoldMessenger.of(context);
final nav =
Navigator.of(ctx);
final nav2 =
Navigator.of(context);
final user =
context.read<User>();
doNetworkRequest(
scaffMgr,
req: () =>
postWithCreadentials(path: 'setAdminStatus', credentials: user, target: user.server, body: {
'room': widget.tag,
'roomServer': widget.server,
'server': item.serverTag,
'name': item.id,
'admin': !item.isAdmin
}),
onOK: (_) {
fetchMembers();
},
after: () {
// close popup
nav.pop();
// close bottom sheet
nav2.pop();
});
},
child: Text(
AppLocalizations.of(context)!
.ok),
)
doNetworkRequest(
scaffMgr,
req: () =>
postWithCreadentials(path: 'setAdminStatus', credentials: user, target: user.server, body: {
'room': widget.tag,
'roomServer': widget.server,
'server': item.serverTag,
'name': item.id,
'admin': !item.isAdmin
}),
onOK: (_) {
fetchMembers();
},
after: () {
// close popup
nav.pop();
// close bottom sheet
nav2.pop();
});
},
child: Text(
AppLocalizations.of(context)!
.ok),
)
],
));
},
)
]
: [],
...((info?.isAdmin)! ||
(info?.isOwner)! ||
((info?.permissions)! &
RoomPermission
.manageMembers !=
0))
? [
ListTile(
leading: const Icon(
Icons.person_remove),
title: Text(
AppLocalizations.of(
context)!
.kickUserTitle),
subtitle: Text(
AppLocalizations.of(
context)!
.kickUserSubtitle),
onTap: () {
// remove user from room
showDialog(
context: context,
builder:
(ctx) =>
AlertDialog(
icon: const Icon(
Icons
.person_remove),
title: Text(AppLocalizations.of(
context)!
.kickUserTitle),
content: Text(AppLocalizations.of(
context)!
.kichUserConfirm(
item.humanReadableName)),
actions: [
TextButton(
onPressed:
() {
// close popup
// NOTE: cancel only closes the dialog
// whilst OK closes both
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 nav2 =
Navigator.of(context);
final user =
context.read<User>();
doNetworkRequest(
scaffMgr,
req: () =>
postWithCreadentials(path: 'kickMember', credentials: user, target: user.server, body: {
'room': widget.tag,
'roomServer': widget.server,
'name': item.id,
'server': item.serverTag
}),
onOK: (_) {
fetchMembers();
},
after: () {
// close popup
nav.pop();
// close bottom sheet
nav2.pop();
});
},
child: Text(
AppLocalizations.of(context)!
.ok),
)
],
));
},
)
]
: [],
],
));
},
)
]
: [],
...((info?.isAdmin)! ||
(info?.isOwner)! ||
((info?.permissions)! &
RoomPermission
.manageMembers !=
0))
? [
ListTile(
leading: const Icon(
Icons.person_remove),
title: Text(
AppLocalizations.of(
context)!
.kickUserTitle),
subtitle: Text(
AppLocalizations.of(
context)!
.kickUserSubtitle),
onTap: () {
// remove user from room
showDialog(
context: context,
builder:
(ctx) =>
AlertDialog(
icon: const Icon(
Icons
.person_remove),
title: Text(AppLocalizations.of(
context)!
.kickUserTitle),
content: Text(AppLocalizations.of(
context)!
.kichUserConfirm(
item.humanReadableName)),
actions: [
TextButton(
onPressed:
() {
// close popup
// NOTE: cancel only closes the dialog
// whilst OK closes both
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 nav2 =
Navigator.of(context);
final user =
context.read<User>();
doNetworkRequest(
scaffMgr,
req: () =>
postWithCreadentials(path: 'kickMember', credentials: user, target: user.server, body: {
'room': widget.tag,
'roomServer': widget.server,
'name': item.id,
'server': item.serverTag
}),
onOK: (_) {
fetchMembers();
},
after: () {
// close popup
nav.pop();
// close bottom sheet
nav2.pop();
});
},
child: Text(
AppLocalizations.of(context)!
.ok),
)
],
));
},
)
]
: [],
],
),
),
FilledButton(
child: Text(
AppLocalizations.of(context)!.close),
onPressed: () {
Navigator.of(context).pop();
},
)
],
),
));
},
),
),
FilledButton(
child: Text(
AppLocalizations.of(context)!.close),
onPressed: () {
Navigator.of(context).pop();
},
)
],
),
));
},
);
},
itemCount: list.length,

View file

@ -0,0 +1 @@

View file

@ -31,10 +31,10 @@ class _EditRoomPermissionSetPageState extends State<EditRoomPermissionSetPage> {
doNetworkRequest(
sm,
req: () => postWithCreadentials(
path: 'getRoomInfo',
credentials: user,
target: user.server,
body: {'room': widget.tag, 'server': widget.server}),
path: 'getRoomInfo',
credentials: user,
target: user.server,
body: {'room': widget.tag, 'server': widget.server}),
onAnyErr: () {
// user should not be here
// close screen
@ -44,7 +44,7 @@ class _EditRoomPermissionSetPageState extends State<EditRoomPermissionSetPage> {
onOK: (body) async {
final info = RoomInfo.fromJSON(body['data']);
setState(() {
permissions = info.permissions;
permissions = info.permissions;
});
return true;
},
@ -72,18 +72,18 @@ class _EditRoomPermissionSetPageState extends State<EditRoomPermissionSetPage> {
final int col = pow(2, index) as int;
return SwitchListTile(
title: Text(RoomPermission.name(item, context)),
subtitle: Text(RoomPermission.describe(item, context)),
onChanged: (state) {
setState(() {
title: Text(RoomPermission.name(item, context)),
subtitle: Text(RoomPermission.describe(item, context)),
onChanged: (state) {
setState(() {
if (state) {
permissions |= col;
} else {
permissions &= ~col;
}
});
},
value: permissions & col != 0);
});
},
value: permissions & col != 0);
},
),
floatingActionButton: FloatingActionButton.extended(
@ -97,18 +97,18 @@ class _EditRoomPermissionSetPageState extends State<EditRoomPermissionSetPage> {
// update permissions
doNetworkRequest(sm,
req: () => postWithCreadentials(
path: 'setRoomRight',
credentials: user,
target: user.server,
body: {
'room': widget.tag,
'server': widget.server,
'rights': permissions
}),
onOK: (_) {
router.pop();
});
req: () => postWithCreadentials(
path: 'setRoomRight',
credentials: user,
target: user.server,
body: {
'room': widget.tag,
'server': widget.server,
'rights': permissions
}),
onOK: (_) {
router.pop();
});
},
),
);

View file

@ -31,11 +31,11 @@ class _EditCategoryPageState extends State<EditCategoryPage> {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (widget.id == null) {
// trying to create a new category
return;
}
fetchCategory();
if (widget.id == null) {
// trying to create a new category
return;
}
fetchCategory();
});
}
@ -45,210 +45,210 @@ class _EditCategoryPageState extends State<EditCategoryPage> {
// 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(() {
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);
.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);
? 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);
// name may not be empty
if (_ctrName.text.isEmpty) {
showSimpleSnackbar(scaffMgr,
text: trans!.errorNoRoomName, action: trans.ok);
return;
}
return;
}
setState(() {
showSpinner = true;
});
setState(() {
showSpinner = true;
});
final user = context.read<User>();
final color = colorIdFromColor(_ctrColor);
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'];
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
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;
});
});
} 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)),
);
// 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)),
);
}
}

View file

@ -17,7 +17,7 @@ class EditItemPage extends StatefulWidget {
final int? item;
const EditItemPage(
{super.key, required this.room, required this.server, this.item});
{super.key, required this.room, required this.server, this.item});
@override
State<StatefulWidget> createState() => _EditItemPageState();
@ -43,20 +43,20 @@ class _EditItemPageState extends State<EditItemPage> {
// TODO: load cached categories first
doNetworkRequest(ScaffoldMessenger.of(context),
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getCategories',
body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async {
final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
.toList();
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getCategories',
body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async {
final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
.toList();
setState(() {
setState(() {
categories = resp;
});
});
});
}
void fetchProducts() {
@ -65,20 +65,20 @@ class _EditItemPageState extends State<EditItemPage> {
// TODO: load cached products first
doNetworkRequest(ScaffoldMessenger.of(context),
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getProducts',
body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async {
final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
.toList();
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getProducts',
body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async {
final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
.toList();
setState(() {
setState(() {
products = resp;
});
});
});
}
void fetchItem() {
@ -87,21 +87,21 @@ class _EditItemPageState extends State<EditItemPage> {
// TODO: load cached item first
doNetworkRequest(ScaffoldMessenger.of(context),
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getItem',
body: {
'room': widget.room,
'server': widget.server,
'listItemID': widget.item
}),
onOK: (body) async {
final resp = RoomItem.fromJSON(body['data']);
setState(() {
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getItem',
body: {
'room': widget.room,
'server': widget.server,
'listItemID': widget.item
}),
onOK: (body) async {
final resp = RoomItem.fromJSON(body['data']);
setState(() {
item = resp;
});
});
});
}
@override
@ -109,12 +109,12 @@ class _EditItemPageState extends State<EditItemPage> {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
fetchCategories();
fetchProducts();
fetchCategories();
fetchProducts();
if (widget.item != null) {
fetchItem();
}
if (widget.item != null) {
fetchItem();
}
});
}
@ -123,156 +123,156 @@ class _EditItemPageState extends State<EditItemPage> {
return Scaffold(
appBar: AppBar(
title: Text((widget.item == null)
? AppLocalizations.of(context)!.createItem
: AppLocalizations.of(context)!.editItem),
? AppLocalizations.of(context)!.createItem
: AppLocalizations.of(context)!.editItem),
),
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: [
Padding(
padding: const EdgeInsets.all(8),
child: TextField(
controller: _ctrName,
keyboardType: TextInputType.name,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.badge),
labelText: AppLocalizations.of(context)!
.inputItemNameLabel,
hintText: AppLocalizations.of(context)!
.inputItemNameHint,
helperText: AppLocalizations.of(context)!
.inputItemNameHelp,
border: const OutlineInputBorder(),
),
),
),
ProductPicker(
label: AppLocalizations.of(context)!
.selectLinkedProductLabel,
hint: AppLocalizations.of(context)!
.selectLinkedProductHint,
products: products,
selected: _ctrLink,
onSelect: (pid) {
setState(() {
_ctrLink = pid;
});
},
),
Padding(
padding: const EdgeInsets.all(8),
child: TextField(
controller: _ctrDescription,
keyboardType: TextInputType.text,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!
.inputItemDescriptionLabel,
hintText: AppLocalizations.of(context)!
.inputItemDescriptionHint,
helperText: AppLocalizations.of(context)!
.inputItemDescriptionHelp,
prefixIcon: const Icon(Icons.dns),
border: const OutlineInputBorder(),
),
),
),
DynamicValueUnitInput(
initialUnit: _ctrUnit,
initialValue: _ctrValue,
onUnitChange: (unit) {
setState(() {
_ctrUnit = unit;
});
},
onValueChange: (value) {
setState(() {
_ctrValue = value;
});
},
),
CategoryPicker(
label: AppLocalizations.of(context)!
.selectCategoryLabel,
hint: AppLocalizations.of(context)!
.selectCategoryHint,
categories: categories,
selected: _ctrCategory,
onSelect: (cid) {
setState(() {
_ctrCategory = cid;
});
},
),
],
))))),
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: [
Padding(
padding: const EdgeInsets.all(8),
child: TextField(
controller: _ctrName,
keyboardType: TextInputType.name,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.badge),
labelText: AppLocalizations.of(context)!
.inputItemNameLabel,
hintText: AppLocalizations.of(context)!
.inputItemNameHint,
helperText: AppLocalizations.of(context)!
.inputItemNameHelp,
border: const OutlineInputBorder(),
),
),
),
ProductPicker(
label: AppLocalizations.of(context)!
.selectLinkedProductLabel,
hint: AppLocalizations.of(context)!
.selectLinkedProductHint,
products: products,
selected: _ctrLink,
onSelect: (pid) {
setState(() {
_ctrLink = pid;
});
},
),
Padding(
padding: const EdgeInsets.all(8),
child: TextField(
controller: _ctrDescription,
keyboardType: TextInputType.text,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!
.inputItemDescriptionLabel,
hintText: AppLocalizations.of(context)!
.inputItemDescriptionHint,
helperText: AppLocalizations.of(context)!
.inputItemDescriptionHelp,
prefixIcon: const Icon(Icons.dns),
border: const OutlineInputBorder(),
),
),
),
DynamicValueUnitInput(
initialUnit: _ctrUnit,
initialValue: _ctrValue,
onUnitChange: (unit) {
setState(() {
_ctrUnit = unit;
});
},
onValueChange: (value) {
setState(() {
_ctrValue = value;
});
},
),
CategoryPicker(
label: AppLocalizations.of(context)!
.selectCategoryLabel,
hint: AppLocalizations.of(context)!
.selectCategoryHint,
categories: categories,
selected: _ctrCategory,
onSelect: (cid) {
setState(() {
_ctrCategory = cid;
});
},
),
],
))))),
floatingActionButton: FloatingActionButton.extended(
onPressed: () async {
final scaffMgr = ScaffoldMessenger.of(context);
final trans = AppLocalizations.of(context);
final nav = Navigator.of(context);
onPressed: () async {
final scaffMgr = ScaffoldMessenger.of(context);
final trans = AppLocalizations.of(context);
final nav = Navigator.of(context);
if (_ctrName.text.isEmpty) {
showSimpleSnackbar(scaffMgr,
text: trans!.errorProductNameShouldNotBeEmpty,
action: trans.ok);
return;
}
if (_ctrName.text.isEmpty) {
showSimpleSnackbar(scaffMgr,
text: trans!.errorProductNameShouldNotBeEmpty,
action: trans.ok);
return;
}
final user = context.read<User>();
final user = context.read<User>();
if (widget.item == null) {
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'addItem',
body: {
'room': widget.room,
'server': widget.server,
'state': 0,
'title': _ctrName.text,
'description': _ctrDescription.text,
'listCatID': _ctrCategory,
'unit': _ctrUnit,
'value': _ctrValue,
'listProdID': _ctrLink
}),
onOK: (_) async {
nav.pop();
});
} else {
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'changeItem',
body: {
'listItemID': widget.item,
'room': widget.room,
'server': widget.server,
'title': _ctrName.text,
'description': _ctrDescription.text,
'listCatID': _ctrCategory,
'defUnit': _ctrUnit,
'defValue': _ctrValue,
'listProdID': _ctrLink
}),
onOK: (_) async {
nav.pop();
});
}
},
label: Text(widget.item != null
? AppLocalizations.of(context)!.editItemShort
: AppLocalizations.of(context)!.createItemShort),
icon: Icon(widget.item != null ? Icons.edit : Icons.add)),
if (widget.item == null) {
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'addItem',
body: {
'room': widget.room,
'server': widget.server,
'state': 0,
'title': _ctrName.text,
'description': _ctrDescription.text,
'listCatID': _ctrCategory,
'unit': _ctrUnit,
'value': _ctrValue,
'listProdID': _ctrLink
}),
onOK: (_) async {
nav.pop();
});
} else {
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'changeItem',
body: {
'listItemID': widget.item,
'room': widget.room,
'server': widget.server,
'title': _ctrName.text,
'description': _ctrDescription.text,
'listCatID': _ctrCategory,
'defUnit': _ctrUnit,
'defValue': _ctrValue,
'listProdID': _ctrLink
}),
onOK: (_) async {
nav.pop();
});
}
},
label: Text(widget.item != null
? AppLocalizations.of(context)!.editItemShort
: AppLocalizations.of(context)!.createItemShort),
icon: Icon(widget.item != null ? Icons.edit : Icons.add)),
);
}
}

View file

@ -24,60 +24,60 @@ class _JoinRoomPageState extends State {
final user = context.read<User>();
doNetworkRequest(null,
req: () => postWithCreadentials(
path: 'listPublicRooms',
credentials: user,
target: user.server,
body: {}),
onOK: (body) async {
// parse rooms
final list = body['data'];
// try to fetch a list of rooms the user is a member of
// use an empty blacklist when request is not successful
final List<Room> blacklist = [];
await doNetworkRequest(sm,
req: () => postWithCreadentials(
path: 'listRooms',
req: () => postWithCreadentials(
path: 'listPublicRooms',
credentials: user,
target: user.server,
body: {}),
onOK: (body) {
final List<Room> list = body['data'].map<Room>((json) {
return Room.fromJSON(json);
}).toList();
for (Room r in list) {
blacklist.add(r);
}
});
onOK: (body) async {
// parse rooms
final list = body['data'];
// process the list of public rooms
final List<Room> builder = [];
processor:
for (dynamic raw in list) {
try {
final room = Room.fromJSON(raw);
// try to fetch a list of rooms the user is a member of
// use an empty blacklist when request is not successful
final List<Room> blacklist = [];
await doNetworkRequest(sm,
req: () => postWithCreadentials(
path: 'listRooms',
credentials: user,
target: user.server,
body: {}),
onOK: (body) {
final List<Room> list = body['data'].map<Room>((json) {
return Room.fromJSON(json);
}).toList();
for (Room r in list) {
blacklist.add(r);
}
});
// figure out if room is on blacklist
// only add room to list,
// if not on blacklist
for (Room r in blacklist) {
if (r == room) {
// server on white list
// move to next iteration on outer for loop
continue processor;
// process the list of public rooms
final List<Room> builder = [];
processor:
for (dynamic raw in list) {
try {
final room = Room.fromJSON(raw);
// figure out if room is on blacklist
// only add room to list,
// if not on blacklist
for (Room r in blacklist) {
if (r == room) {
// server on white list
// move to next iteration on outer for loop
continue processor;
}
}
}
builder.add(room);
} catch (_) {
// ignore room
builder.add(room);
} catch (_) {
// ignore room
}
}
}
setState(() {
setState(() {
rooms = builder;
});
});
});
}
@override
@ -90,8 +90,8 @@ class _JoinRoomPageState extends State {
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context)
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
double width = MediaQuery.of(context).size.width;
double height = MediaQuery.of(context).size.height;
@ -118,182 +118,189 @@ class _JoinRoomPageState extends State {
},
),
MenuAnchor(
builder: (ctx, controller, child) {
return IconButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
icon: const Icon(Icons.more_vert),
);
},
menuChildren: [
MenuItemButton(
leadingIcon: const Icon(Icons.drafts),
child: Text(AppLocalizations.of(context)!.joinRoomInvite),
onPressed: () {
// show settings screen
context.goNamed('join-room-ota');
}),
])
builder: (ctx, controller, child) {
return IconButton(
onPressed: () {
if (controller.isOpen) {
controller.close();
} else {
controller.open();
}
},
icon: const Icon(Icons.more_vert),
);
},
menuChildren: [
MenuItemButton(
leadingIcon: const Icon(Icons.drafts),
child: Text(AppLocalizations.of(context)!.joinRoomInvite),
onPressed: () {
// show settings screen
context.goNamed('join-room-ota');
}),
])
],
),
body: rooms.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(AppLocalizations.of(context)!.noNewRoomsFound, style: textTheme.titleLarge),
],
))
: ListView.builder(
itemCount: rooms.length,
itemBuilder: (ctx, i) {
final room = rooms[i];
return Card(
margin: const EdgeInsets.all(8.0),
clipBehavior: Clip.antiAliasWithSaveLayer,
semanticContainer: true,
child: InkWell(
onTap: () {
// show modalBottomSheet
// with room information
// and join button
showModalBottomSheet(
context: ctx,
builder: (ctx) {
return BottomSheet(
onClosing: () {},
builder: (ctx) {
return Column(
crossAxisAlignment:
CrossAxisAlignment.center,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(14),
child: Column(children: [
// room icon
SvgPicture.asset(
(room.icon?.img)!,
width: smallest * 0.2,
height: smallest * 0.2,
),
// room name
Text(
room.name,
style: textTheme.displayMedium,
),
Text(
'${room.id}@${room.serverTag}',
style: textTheme.labelSmall,
),
// description
Text(room.description,
style: textTheme.bodyLarge),
// visibility
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(room.visibility?.icon),
Text((room
.visibility?.text(context))!),
]),
])),
// action buttons
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
// cancel button
Padding(
padding:
const EdgeInsets.all(14),
child: ElevatedButton.icon(
icon:
const Icon(Icons.close),
label: Text(AppLocalizations.of(context)!.cancel),
onPressed: () {
// close sheet
Navigator.pop(context);
},
)),
// join room button
Padding(
padding:
const EdgeInsets.all(14),
child: FilledButton.icon(
icon:
const Icon(Icons.check),
label: Text(AppLocalizations.of(context)!.joinRoom),
onPressed: () async {
final scaffMgr =
ScaffoldMessenger.of(
context);
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(AppLocalizations.of(context)!.noNewRoomsFound,
style: textTheme.titleLarge),
],
))
: ListView.builder(
itemCount: rooms.length,
itemBuilder: (ctx, i) {
final room = rooms[i];
return Card(
margin: const EdgeInsets.all(8.0),
clipBehavior: Clip.antiAliasWithSaveLayer,
semanticContainer: true,
child: InkWell(
onTap: () {
// show modalBottomSheet
// with room information
// and join button
showModalBottomSheet(
context: ctx,
builder: (ctx) {
return BottomSheet(
onClosing: () {},
builder: (ctx) {
return Column(
crossAxisAlignment:
CrossAxisAlignment.center,
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(14),
child: Column(children: [
// room icon
SvgPicture.asset(
(room.icon?.img)!,
width: smallest * 0.2,
height: smallest * 0.2,
),
// room name
Text(
room.name,
style: textTheme.displayMedium,
),
Text(
'${room.id}@${room.serverTag}',
style: textTheme.labelSmall,
),
// description
Text(room.description,
style: textTheme.bodyLarge),
// visibility
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(room.visibility?.icon),
Text((room.visibility
?.text(context))!),
]),
])),
// action buttons
Row(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
// cancel button
Padding(
padding:
const EdgeInsets.all(14),
child: ElevatedButton.icon(
icon:
const Icon(Icons.close),
label: Text(
AppLocalizations.of(
context)!
.cancel),
onPressed: () {
// close sheet
Navigator.pop(context);
},
)),
// join room button
Padding(
padding:
const EdgeInsets.all(14),
child: FilledButton.icon(
icon:
const Icon(Icons.check),
label: Text(
AppLocalizations.of(
context)!
.joinRoom),
onPressed: () async {
final scaffMgr =
ScaffoldMessenger.of(
context);
final nav =
Navigator.of(context);
final user =
context.read<User>();
final router =
GoRouter.of(context);
final nav =
Navigator.of(context);
final user =
context.read<User>();
final router =
GoRouter.of(context);
doNetworkRequest(scaffMgr,
req: () =>
postWithCreadentials(
credentials:
user,
target: user
.server,
path:
'joinPublicRoom',
body: {
'room':
room.id,
'server': room
.serverTag
}),
onOK: (body) async {
await room.toDisk();
nav.pop();
router
.pushReplacementNamed(
'room',
params: {
'server': room
.serverTag,
'id': room.id
});
});
},
))
])
],
);
},
);
});
doNetworkRequest(scaffMgr,
req: () =>
postWithCreadentials(
credentials:
user,
target: user
.server,
path:
'joinPublicRoom',
body: {
'room':
room.id,
'server': room
.serverTag
}),
onOK: (body) async {
await room.toDisk();
nav.pop();
router
.pushReplacementNamed(
'room',
params: {
'server': room
.serverTag,
'id': room.id
});
});
},
))
])
],
);
},
);
});
},
child: Container(
padding: const EdgeInsets.fromLTRB(10, 5, 5, 10),
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,
))));
},
child: Container(
padding: const EdgeInsets.fromLTRB(10, 5, 5, 10),
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(
label: Text(AppLocalizations.of(context)!.newRoom),
icon: const Icon(Icons.add),

View file

@ -34,72 +34,69 @@ class _RoomPageState extends State<RoomPage> {
try {
final diskRoom =
await Room.fromDisk(serverTag: widget.server, id: widget.tag);
await Room.fromDisk(serverTag: widget.server, id: widget.tag);
setState(() {
room = diskRoom;
room = diskRoom;
});
} catch (_) {}
doNetworkRequest(sm,
req: () => postWithCreadentials(
path: 'getRoomInfo',
credentials: user,
target: user.server,
body: {'room': widget.tag, 'server': widget.server}),
onOK: (body) async {
final info = RoomInfo.fromJSON(body['data']);
final room = Room.fromJSON(body['data']);
req: () => postWithCreadentials(
path: 'getRoomInfo',
credentials: user,
target: user.server,
body: {'room': widget.tag, 'server': widget.server}),
onOK: (body) async {
final info = RoomInfo.fromJSON(body['data']);
final room = Room.fromJSON(body['data']);
room.toDisk();
room.toDisk();
setState(() {
setState(() {
this.info = info;
});
return true;
},
onNetworkErr: () {
// user offline
if (room == null) {
// no room data available
// NOTE: close room?
}
return true;
},
onServerErr: (json) {
// user no longer in room
// TODO: close room
return true;
});
return true;
},
onNetworkErr: () {
// user offline
if (room == null) {
// no room data available
// NOTE: close room?
}
return true;
},
onServerErr: (json) {
// user no longer in room
// TODO: close room
return true;
});
}
@override
void initState() {
super.initState();
room = Room(
id: widget.tag,
serverTag: widget.server
);
room = Room(id: widget.tag, serverTag: widget.server);
_ctr.addListener(() {
setState(() {
page = _ctr.page?.toInt() ?? _ctr.initialPage;
});
setState(() {
page = _ctr.page?.toInt() ?? _ctr.initialPage;
});
});
Room.listen((_) async {
// rooms changed on disk
// probably this one,
// because it is currently open
// NOTE: might be a different room
// (if a background listener is implemented at some point,
// checking if this room changed might improve performance)
try {
final r = await Room.fromDisk(serverTag: widget.server, id: widget.tag);
setState(() {
room = r;
});
} catch (_) {}
// rooms changed on disk
// probably this one,
// because it is currently open
// NOTE: might be a different room
// (if a background listener is implemented at some point,
// checking if this room changed might improve performance)
try {
final r = await Room.fromDisk(serverTag: widget.server, id: widget.tag);
setState(() {
room = r;
});
} catch (_) {}
});
WidgetsBinding.instance.addPostFrameCallback((_) => fetchInfo());
@ -123,27 +120,27 @@ class _RoomPageState extends State<RoomPage> {
bottomNavigationBar: NavigationBar(
onDestinationSelected: (int index) {
_ctr.animateToPage(index,
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 300));
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 300));
},
selectedIndex: page,
destinations: [
NavigationDestination(
icon: const Icon(Icons.list),
label: AppLocalizations.of(context)!.roomListTitle,
tooltip: AppLocalizations.of(context)!.roomListSubtitle),
icon: const Icon(Icons.list),
label: AppLocalizations.of(context)!.roomListTitle,
tooltip: AppLocalizations.of(context)!.roomListSubtitle),
NavigationDestination(
icon: const Icon(Icons.inventory_2),
label: AppLocalizations.of(context)!.roomProductsTitle,
tooltip: AppLocalizations.of(context)!.roomProductsSubtitle),
icon: const Icon(Icons.inventory_2),
label: AppLocalizations.of(context)!.roomProductsTitle,
tooltip: AppLocalizations.of(context)!.roomProductsSubtitle),
NavigationDestination(
icon: const Icon(Icons.category),
label: AppLocalizations.of(context)!.roomCategoriesTitle,
tooltip: AppLocalizations.of(context)!.roomCategoriesSubtitle),
icon: const Icon(Icons.category),
label: AppLocalizations.of(context)!.roomCategoriesTitle,
tooltip: AppLocalizations.of(context)!.roomCategoriesSubtitle),
NavigationDestination(
icon: const Icon(Icons.info_rounded),
label: AppLocalizations.of(context)!.roomAboutTitle,
tooltip: AppLocalizations.of(context)!.roomAboutSubtitle),
icon: const Icon(Icons.info_rounded),
label: AppLocalizations.of(context)!.roomAboutTitle,
tooltip: AppLocalizations.of(context)!.roomAboutSubtitle),
],
),
);

View file

@ -38,7 +38,7 @@ class _NewRoomPageState extends State<NewRoomPage> {
_ctrIcon = room.icon!;
setState(() {
this.room = room;
this.room = room;
});
}
@ -50,17 +50,17 @@ class _NewRoomPageState extends State<NewRoomPage> {
try {
final diskRoom =
await Room.fromDisk(serverTag: widget.server!, id: widget.tag!);
await Room.fromDisk(serverTag: widget.server!, id: widget.tag!);
initFromRoom(diskRoom);
} catch (_) {}
doNetworkRequest(
sm,
req: () => postWithCreadentials(
path: 'getRoomInfo',
credentials: user,
target: user.server,
body: {'room': widget.tag, 'server': widget.server}),
path: 'getRoomInfo',
credentials: user,
target: user.server,
body: {'room': widget.tag, 'server': widget.server}),
onOK: (body) async {
final room = Room.fromJSON(body['data']);
room.toDisk();
@ -70,9 +70,9 @@ class _NewRoomPageState extends State<NewRoomPage> {
// no room data available
// use data from disk
(() async {
// no room data available
// close screen
router.pushReplacementNamed('home');
// no room data available
// close screen
router.pushReplacementNamed('home');
})();
return true;
},
@ -89,9 +89,9 @@ class _NewRoomPageState extends State<NewRoomPage> {
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
if (isEditPage()) {
fetchInfo();
}
if (isEditPage()) {
fetchInfo();
}
});
}
@ -102,235 +102,233 @@ class _NewRoomPageState extends State<NewRoomPage> {
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context)
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
.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(isEditPage()
? AppLocalizations.of(context)!.editRoomMetadata
: AppLocalizations.of(context)!.newRoom),
),
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: SvgPicture.asset(
_ctrIcon.img,
width: smallest * 0.3,
height: smallest * 0.3,
),
tooltip: AppLocalizations.of(context)!
.changeRoomIcon,
onPressed: () {
showDialog(
context: context,
builder: (ctx) =>
RoomIconPicker(onSelect: (icon) {
setState(() {
_ctrIcon = icon;
});
context.pop();
}));
}),
Padding(
padding: const EdgeInsets.all(8),
child: TextField(
enabled: !isEditPage(),
controller: _ctrID,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.fact_check),
labelText: AppLocalizations.of(context)!
.inputRoomIdLabel,
hintText: AppLocalizations.of(context)!
.inputRoomIdHint,
helperText: AppLocalizations.of(context)!
.inputRoomIdHelp,
border: const OutlineInputBorder(),
),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: TextField(
controller: _ctrName,
keyboardType: TextInputType.name,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.badge),
labelText: AppLocalizations.of(context)!
.inputRoomNameLabel,
hintText: AppLocalizations.of(context)!
.inputRoomNameHint,
helperText: AppLocalizations.of(context)!
.inputRoomNameHelp,
border: const OutlineInputBorder(),
),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: TextField(
controller: _ctrDescription,
keyboardType: TextInputType.text,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!
.inputRoomDescriptionLabel,
hintText: AppLocalizations.of(context)!
.inputRoomDescriptionHint,
helperText: AppLocalizations.of(context)!
.inputRoomDescriptionHelp,
prefixIcon: const Icon(Icons.dns),
border: const OutlineInputBorder(),
),
),
),
...(!isEditPage())
? [
Text(
AppLocalizations.of(context)!
.roomVisibilityTitle,
style: textTheme.labelLarge),
Text(
AppLocalizations.of(context)!
.roomVisibilitySubtitle,
style: textTheme.bodySmall),
SegmentedButton<RoomVisibility>(
showSelectedIcon: true,
multiSelectionEnabled: false,
emptySelectionAllowed: false,
segments:
RoomVisibility.list().map((vis) {
return ButtonSegment<
RoomVisibility>(
value: vis,
label: Text(vis.text(context)),
icon: Icon(vis.icon));
}).toList(),
onSelectionChanged: ((vset) {
setState(() {
_ctrVis = vset.single;
});
}),
selected: {_ctrVis},
selectedIcon: Icon(_ctrVis.icon),
),
]
: []
],
))))),
floatingActionButton: FloatingActionButton.extended(
onPressed: () async {
final scaffMgr = ScaffoldMessenger.of(context);
final router = GoRouter.of(context);
final trans = AppLocalizations.of(context);
? 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(isEditPage()
? AppLocalizations.of(context)!.editRoomMetadata
: AppLocalizations.of(context)!.newRoom),
),
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: SvgPicture.asset(
_ctrIcon.img,
width: smallest * 0.3,
height: smallest * 0.3,
),
tooltip: AppLocalizations.of(context)!
.changeRoomIcon,
onPressed: () {
showDialog(
context: context,
builder: (ctx) =>
RoomIconPicker(onSelect: (icon) {
setState(() {
_ctrIcon = icon;
});
context.pop();
}));
}),
Padding(
padding: const EdgeInsets.all(8),
child: TextField(
enabled: !isEditPage(),
controller: _ctrID,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.fact_check),
labelText: AppLocalizations.of(context)!
.inputRoomIdLabel,
hintText: AppLocalizations.of(context)!
.inputRoomIdHint,
helperText: AppLocalizations.of(context)!
.inputRoomIdHelp,
border: const OutlineInputBorder(),
),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: TextField(
controller: _ctrName,
keyboardType: TextInputType.name,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.badge),
labelText: AppLocalizations.of(context)!
.inputRoomNameLabel,
hintText: AppLocalizations.of(context)!
.inputRoomNameHint,
helperText: AppLocalizations.of(context)!
.inputRoomNameHelp,
border: const OutlineInputBorder(),
),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: TextField(
controller: _ctrDescription,
keyboardType: TextInputType.text,
decoration: InputDecoration(
labelText: AppLocalizations.of(context)!
.inputRoomDescriptionLabel,
hintText: AppLocalizations.of(context)!
.inputRoomDescriptionHint,
helperText: AppLocalizations.of(context)!
.inputRoomDescriptionHelp,
prefixIcon: const Icon(Icons.dns),
border: const OutlineInputBorder(),
),
),
),
...(!isEditPage())
? [
Text(
AppLocalizations.of(context)!
.roomVisibilityTitle,
style: textTheme.labelLarge),
Text(
AppLocalizations.of(context)!
.roomVisibilitySubtitle,
style: textTheme.bodySmall),
SegmentedButton<RoomVisibility>(
showSelectedIcon: true,
multiSelectionEnabled: false,
emptySelectionAllowed: false,
segments:
RoomVisibility.list().map((vis) {
return ButtonSegment<
RoomVisibility>(
value: vis,
label: Text(vis.text(context)),
icon: Icon(vis.icon));
}).toList(),
onSelectionChanged: ((vset) {
setState(() {
_ctrVis = vset.single;
});
}),
selected: {_ctrVis},
selectedIcon: Icon(_ctrVis.icon),
),
]
: []
],
))))),
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);
// name may not be empty
if (_ctrName.text.isEmpty) {
showSimpleSnackbar(scaffMgr,
text: trans!.errorNoRoomName, action: trans.ok);
return;
}
return;
}
final user = context.read<User>();
final user = context.read<User>();
if (isEditPage()) {
final nav = Navigator.of(context);
Room clone = room!;
clone.name = _ctrName.text;
clone.description = _ctrDescription.text;
clone.icon = _ctrIcon;
if (isEditPage()) {
final nav = Navigator.of(context);
Room clone = room!;
clone.name = _ctrName.text;
clone.description = _ctrDescription.text;
clone.icon = _ctrIcon;
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
target: user.server,
credentials: user,
path: 'changeRoomMeta',
body: {
'room': clone.id,
'server': clone.serverTag,
'title': clone.name,
'description': clone.description,
'icon': clone.icon?.type,
}),
onOK: (_) async {
// room was created
// save room
await clone.toDisk();
nav.pop();
});
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
target: user.server,
credentials: user,
path: 'changeRoomMeta',
body: {
'room': clone.id,
'server': clone.serverTag,
'title': clone.name,
'description': clone.description,
'icon': clone.icon?.type,
}),
onOK: (_) async {
// room was created
// save room
await clone.toDisk();
nav.pop();
});
} else {
// new room specific tests & request
} else {
// new room specific tests & request
// ID should be at least three characters long
if (_ctrID.text.length < 3) {
showSimpleSnackbar(scaffMgr,
text: _ctrID.text.isEmpty
? trans!.errorNoRoomId
: trans!.errorRoomIdLength,
action: trans.ok);
// ID should be at least three characters long
if (_ctrID.text.length < 3) {
showSimpleSnackbar(scaffMgr,
text: _ctrID.text.isEmpty
? trans!.errorNoRoomId
: trans!.errorRoomIdLength,
action: trans.ok);
return;
}
return;
}
final room = Room(
id: _ctrID.text,
serverTag: user.server.tag,
name: _ctrName.text,
description: _ctrDescription.text,
icon: _ctrIcon,
visibility: _ctrVis);
final room = Room(
id: _ctrID.text,
serverTag: user.server.tag,
name: _ctrName.text,
description: _ctrDescription.text,
icon: _ctrIcon,
visibility: _ctrVis);
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
target: user.server,
credentials: user,
path: 'createRoom',
body: {
'room': room.id,
'title': room.name,
'description': room.description,
'icon': room.icon?.type,
'visibility': room.visibility?.type
}),
onOK: (_) async {
// room was created
// save room
await room.toDisk();
// move to home page
router.pushReplacementNamed('home');
});
}
},
label: Text(isEditPage()
? AppLocalizations.of(context)!.editRoomMetadataShort
: AppLocalizations.of(context)!.createRoomShort),
icon: Icon(isEditPage() ? Icons.edit : Icons.add)),
);
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
target: user.server,
credentials: user,
path: 'createRoom',
body: {
'room': room.id,
'title': room.name,
'description': room.description,
'icon': room.icon?.type,
'visibility': room.visibility?.type
}),
onOK: (_) async {
// room was created
// save room
await room.toDisk();
// move to home page
router.pushReplacementNamed('home');
});
}
},
label: Text(isEditPage()
? AppLocalizations.of(context)!.editRoomMetadataShort
: AppLocalizations.of(context)!.createRoomShort),
icon: Icon(isEditPage() ? Icons.edit : Icons.add)),
);
}
}

View file

@ -24,19 +24,19 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context)
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
.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(width, height);
return SingleChildScrollView(
child: Center(
child: Column(children: [
// room meta display
...(widget.room != null)
? [
child: Center(
child: Column(children: [
// room meta display
...(widget.room != null)
? [
Padding(
padding: const EdgeInsets.all(14),
child: Column(
@ -60,27 +60,27 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
textAlign: TextAlign.center,
),
Padding(
padding: const EdgeInsets.all(8),
child: SegmentedButton<int>(
showSelectedIcon: true,
multiSelectionEnabled: false,
emptySelectionAllowed: false,
segments: RoomVisibility.list().map((vis) {
padding: const EdgeInsets.all(8),
child: SegmentedButton<int>(
showSelectedIcon: true,
multiSelectionEnabled: false,
emptySelectionAllowed: false,
segments: RoomVisibility.list().map((vis) {
return ButtonSegment<int>(
value: vis.type,
label: Text(vis.text(context)),
icon: Icon(vis.icon));
}).toList(),
onSelectionChanged: ((vset) {
value: vis.type,
label: Text(vis.text(context)),
icon: Icon(vis.icon));
}).toList(),
onSelectionChanged: ((vset) {
// check permission
// only show confirm dialog when user
// is admin, owner or has CHANGE_ADMIN permission
if (widget.info == null ||
(!(widget.info?.isAdmin ?? false) &&
!(widget.info?.isOwner ?? false) &&
((widget.info?.permissions)! &
RoomPermission.ota ==
0))) {
(!(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
@ -89,226 +89,247 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
final vis = RoomVisibility(vset.first);
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(AppLocalizations.of(context)!.changeRoomVisibilityTitle),
content: Text(AppLocalizations.of(context)!.changeRoomVisibilitySubtitle(vis.text(context))),
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
context: context,
builder: (ctx) => AlertDialog(
title: Text(AppLocalizations.of(context)!
.changeRoomVisibilityTitle),
content: Text(
AppLocalizations.of(context)!
.changeRoomVisibilitySubtitle(
vis.text(context))),
actions: [
TextButton(
onPressed: () {
context.pop();
},
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)!},
selectedIcon: Icon((widget.room?.visibility?.icon)!),
)),
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)!},
selectedIcon: Icon((widget.room?.visibility?.icon)!),
)),
],
),
)
]
: [],
: [],
Padding(
padding: const EdgeInsets.all(14),
child: Column(
children: [
// edit room meta button
...(widget.info != null &&
((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) ||
((widget.info?.permissions)! &
RoomPermission.changeMeta !=
0)))
Padding(
padding: const EdgeInsets.all(14),
child: Column(
children: [
// edit room meta button
...(widget.info != null &&
((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) ||
((widget.info?.permissions)! &
RoomPermission.changeMeta !=
0)))
? [
ListTile(
trailing: const Icon(Icons.chevron_right),
title: Text(AppLocalizations.of(context)!.editRoomMetadata),
subtitle: Text(AppLocalizations.of(context)!.editRoomMetadataSubtitle),
onTap: () {
// show edit room screen
context.goNamed('edit-room', params: {
ListTile(
trailing: const Icon(Icons.chevron_right),
title: Text(
AppLocalizations.of(context)!.editRoomMetadata),
subtitle: Text(AppLocalizations.of(context)!
.editRoomMetadataSubtitle),
onTap: () {
// show edit room screen
context.goNamed('edit-room', params: {
'server': (widget.room?.serverTag)!,
'id': (widget.room?.id)!
});
},
),
]
});
},
),
]
: [],
// open members view
ListTile(
trailing: const Icon(Icons.chevron_right),
title: Text(AppLocalizations.of(context)!.showRoomMembers),
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)))
// open members view
ListTile(
trailing: const Icon(Icons.chevron_right),
title: Text(AppLocalizations.of(context)!.showRoomMembers),
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: {
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)))
...(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: {
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: {
});
},
),
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)
? [
...(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>();
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':
(widget.room?.serverTag)!,
},
credentials: user),
onOK: (_) async {
// try delete room from disk
try {
await widget.room?.removeDisk();
} catch (_) {}
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),
)
],
));
},
))
// 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

@ -26,7 +26,7 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
fetchCategories();
fetchCategories();
});
}
@ -36,29 +36,29 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
// TODO: load cached rooms
doNetworkRequest(ScaffoldMessenger.of(context),
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getCategories',
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (json) {
final resp = json['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
.toList();
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getCategories',
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (json) {
final resp = json['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
.toList();
if (mounted) {
setState(() {
if (mounted) {
setState(() {
list = resp;
});
}
});
});
}
});
}
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context)
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
return Scaffold(
body: ReorderableListView.builder(
@ -70,13 +70,13 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
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,
(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
@ -86,8 +86,8 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
if (!((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) ||
((widget.info?.permissions)! &
RoomPermission.editRoomContent !=
0))) {
RoomPermission.editRoomContent !=
0))) {
// user is not allowed to edit or delete categories
return;
}
@ -96,104 +96,104 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
context: context,
builder: (context) => BottomSheet(
builder: (context) => Column(children: [
Padding(
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),
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();
)),
// 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(
// 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(
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
fetchCategories();
},
after: () {
icon: const Icon(Icons.delete),
title: Text(AppLocalizations.of(context)!
.deleteCategory),
content: Text(AppLocalizations.of(context)!
.deleteCategoryConfirm(item.name)),
actions: [
TextButton(
onPressed: () {
// close popup
navInner.pop();
// close modal bottom sheet
nav.pop();
});
},
child: Text(AppLocalizations.of(context)!
.deleteCategory),
)
],
));
},
),
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
fetchCategories();
},
after: () {
// close popup
navInner.pop();
// close modal bottom sheet
nav.pop();
});
},
child: Text(AppLocalizations.of(context)!
.deleteCategory),
)
],
));
},
),
]),
onClosing: () {},
),
@ -206,50 +206,52 @@ class _RoomCategoriesPageState extends State<RoomCategoriesPage> {
if (!((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) ||
((widget.info?.permissions)! & RoomPermission.editRoomContent !=
0))) {
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);
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),
// 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()
}));
credentials: user,
target: user.server,
path: 'changeCategoriesOrder',
body: {
'room': widget.room?.id,
'server': widget.room?.serverTag,
'listCatIDs': list.map((item) => item.id).toList()
}));
});
},
),
floatingActionButton: (widget.info != null && ((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: () {
// show new category popup
context.pushNamed('new-category', params: {
'server': widget.room!.serverTag,
'id': widget.room!.id,
});
},
)
: null,
floatingActionButton: (widget.info != null &&
((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: () {
// show new category popup
context.pushNamed('new-category', params: {
'server': widget.room!.serverTag,
'id': widget.room!.id,
});
},
)
: null,
);
}
}

View file

@ -36,74 +36,74 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
// TODO: load cached items first
doNetworkRequest(ScaffoldMessenger.of(context),
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getItems',
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async {
final resp = body['data']
.map<RoomItem>((raw) => RoomItem.fromJSON(raw))
.toList();
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getItems',
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async {
final resp = body['data']
.map<RoomItem>((raw) => RoomItem.fromJSON(raw))
.toList();
final List<RoomItem> l = [];
final List<RoomItem> c = [];
final List<RoomItem> l = [];
final List<RoomItem> c = [];
for (RoomItem item in resp) {
if (item.state == 0) {
l.add(item);
} else {
c.add(item);
for (RoomItem item in resp) {
if (item.state == 0) {
l.add(item);
} else {
c.add(item);
}
}
}
// TODO: cache items
// TODO: cache items
if (mounted) {
setState(() {
if (mounted) {
setState(() {
list = l;
cart = c;
sortAll();
});
}
});
});
}
});
}
void sortAll() {
for (List<RoomItem> input in [list, cart]) {
setState(() {
input.sort((a, b) {
if (a.category == b.category) {
return 0;
}
if (a.category == null) {
// b should be below
return -1;
}
if (b.category == null) {
// a should be below
return 1;
}
input.sort((a, b) {
if (a.category == b.category) {
return 0;
}
if (a.category == null) {
// b should be below
return -1;
}
if (b.category == null) {
// a should be below
return 1;
}
final weightA = weights[a.category];
final weightB = weights[b.category];
// both could be null now,
// so we have to check agein
if (weightA == weightB) {
return 0;
}
if (weightA == null) {
// b should be below
return -1;
}
if (weightB == null) {
// a should be below
return 1;
}
final weightA = weights[a.category];
final weightB = weights[b.category];
// both could be null now,
// so we have to check agein
if (weightA == weightB) {
return 0;
}
if (weightA == null) {
// b should be below
return -1;
}
if (weightB == null) {
// a should be below
return 1;
}
return weightA.compareTo(weightB);
});
return weightA.compareTo(weightB);
});
});
}
}
@ -114,31 +114,31 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
// TODO: load cached categories first
doNetworkRequest(ScaffoldMessenger.of(context),
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getCategories',
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async {
final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
.toList();
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getCategories',
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async {
final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
.toList();
Map<int, int> map = {};
Map<int?, RoomCategory> cat = {};
for (int i = 0; i < resp.length; i++) {
map[resp[i].id] = i;
cat[resp[i].id] = resp[i];
}
Map<int, int> map = {};
Map<int?, RoomCategory> cat = {};
for (int i = 0; i < resp.length; i++) {
map[resp[i].id] = i;
cat[resp[i].id] = resp[i];
}
if (mounted) {
setState(() {
if (mounted) {
setState(() {
weights = map;
categories = cat;
sortAll();
});
}
});
});
}
});
}
void fetchProducts() {
@ -147,22 +147,22 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
// TODO: load cached products first
doNetworkRequest(ScaffoldMessenger.of(context),
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getProducts',
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async {
final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
.toList();
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getProducts',
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async {
final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
.toList();
if (mounted) {
setState(() {
if (mounted) {
setState(() {
products = resp;
});
}
});
});
}
});
}
@override
@ -170,41 +170,41 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
fetchItems();
fetchCategories();
fetchProducts();
fetchItems();
fetchCategories();
fetchProducts();
});
}
void changeItemState(RoomItem item) {
final user = context.read<User>();
doNetworkRequest(ScaffoldMessenger.of(context),
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'changeItemState',
body: {
'room': widget.room?.id,
'server': widget.room?.serverTag,
'listItemID': item.id,
'state': item.state
}));
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'changeItemState',
body: {
'room': widget.room?.id,
'server': widget.room?.serverTag,
'listItemID': item.id,
'state': item.state
}));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: ListView(children: [
LabeledDivider(AppLocalizations.of(context)!.shoppingList),
ListView.builder(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
itemCount: list.length,
itemBuilder: (context, index) {
final item = list[index];
final cat =
categories[item.category] ?? RoomCategory.other(context);
return ShoppingListItem(
LabeledDivider(AppLocalizations.of(context)!.shoppingList),
ListView.builder(
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
itemCount: list.length,
itemBuilder: (context, index) {
final item = list[index];
final cat =
categories[item.category] ?? RoomCategory.other(context);
return ShoppingListItem(
name: item.name,
description: item.description,
category: cat,
@ -215,9 +215,9 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
item.state = 1;
setState(() {
list.removeAt(index);
cart.add(item);
sortAll();
list.removeAt(index);
cart.add(item);
sortAll();
});
// network request
@ -235,28 +235,28 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
// - delete item (if allowed)
// - move to/from shopping cart?
showModalBottomSheet(
context: context,
builder: (context) => ShoppingListItemInfo(
products: products,
category: cat,
info: widget.info,
item: item,
room: widget.room!.id,
server: widget.room!.serverTag));
});
},
),
LabeledDivider(AppLocalizations.of(context)!.shoppingCart),
ListView.builder(
itemCount: cart.length,
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
itemBuilder: (context, index) {
final item = cart[index];
final cat =
categories[item.category] ?? RoomCategory.other(context);
context: context,
builder: (context) => ShoppingListItemInfo(
products: products,
category: cat,
info: widget.info,
item: item,
room: widget.room!.id,
server: widget.room!.serverTag));
});
},
),
LabeledDivider(AppLocalizations.of(context)!.shoppingCart),
ListView.builder(
itemCount: cart.length,
shrinkWrap: true,
physics: const ClampingScrollPhysics(),
itemBuilder: (context, index) {
final item = cart[index];
final cat =
categories[item.category] ?? RoomCategory.other(context);
return ShoppingListItem(
return ShoppingListItem(
name: item.name,
description: item.description,
category: cat,
@ -266,9 +266,9 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
// move back to list
item.state = 0;
setState(() {
cart.removeAt(index);
list.add(item);
sortAll();
cart.removeAt(index);
list.add(item);
sortAll();
});
// network request
@ -286,37 +286,37 @@ class _ShoppingListPageState extends State<ShoppingListPage> {
// - delete item (if allowed)
// - move to/from shopping cart?
showModalBottomSheet(
context: context,
builder: (context) => ShoppingListItemInfo(
products: products,
category: cat,
item: item,
info: widget.info,
room: widget.room!.id,
server: widget.room!.serverTag));
});
},
)
context: context,
builder: (context) => ShoppingListItemInfo(
products: products,
category: cat,
item: item,
info: widget.info,
room: widget.room!.id,
server: widget.room!.serverTag));
});
},
)
]),
floatingActionButton: (widget.info != null &&
((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) ||
((widget.info?.permissions)! &
RoomPermission.addShoppingListItems !=
0)))
? FloatingActionButton.extended(
icon: const Icon(Icons.add),
label: Text(AppLocalizations.of(context)!.newItemShort),
tooltip: AppLocalizations.of(context)!.newItemLong,
onPressed: () {
// show new category popup
context.pushNamed('new-item', params: {
'server': widget.room!.serverTag,
'id': widget.room!.id,
});
},
)
: null,
((widget.info?.isAdmin ?? false) ||
(widget.info?.isOwner ?? false) ||
((widget.info?.permissions)! &
RoomPermission.addShoppingListItems !=
0)))
? FloatingActionButton.extended(
icon: const Icon(Icons.add),
label: Text(AppLocalizations.of(context)!.newItemShort),
tooltip: AppLocalizations.of(context)!.newItemLong,
onPressed: () {
// show new category popup
context.pushNamed('new-item', params: {
'server': widget.room!.serverTag,
'id': widget.room!.id,
});
},
)
: null,
);
}
}
@ -331,14 +331,14 @@ class ShoppingListItem extends StatelessWidget {
final Function()? onTap;
const ShoppingListItem(
{required this.name,
{required this.name,
required this.category,
required this.inCart,
required this.description,
required key,
this.onDismiss,
this.onTap})
: _key = key;
: _key = key;
@override
Widget build(BuildContext context) {
@ -360,21 +360,21 @@ class ShoppingListItem extends StatelessWidget {
return true;
},
background:
Icon(!inCart ? Icons.shopping_cart : Icons.remove_shopping_cart),
Icon(!inCart ? Icons.shopping_cart : Icons.remove_shopping_cart),
child: Opacity(
opacity: inCart ? 0.5 : 1.0,
child: ListTile(
title: Text(name),
subtitle: Text(description),
trailing: CategoryChip(
category: category,
),
onTap: () {
if (onTap != null) {
onTap!();
}
},
)),
opacity: inCart ? 0.5 : 1.0,
child: ListTile(
title: Text(name),
subtitle: Text(description),
trailing: CategoryChip(
category: category,
),
onTap: () {
if (onTap != null) {
onTap!();
}
},
)),
);
}
}
@ -388,7 +388,7 @@ class ShoppingListItemInfo extends StatelessWidget {
final List<RoomProduct> products;
const ShoppingListItemInfo(
{super.key,
{super.key,
this.info,
required this.item,
required this.server,
@ -404,88 +404,88 @@ class ShoppingListItemInfo extends StatelessWidget {
builder: (context) => Column(
children: [
Padding(
padding: const EdgeInsets.all(14),
child: Center(
child: Column(children: [
Text(item.name, style: textTheme.headlineLarge),
Text(item.description, style: textTheme.titleMedium),
CategoryChip(
category: category,
),
Text(Unit.fromId(item.unit).display(context, item.value))
]))),
padding: const EdgeInsets.all(14),
child: Center(
child: Column(children: [
Text(item.name, style: textTheme.headlineLarge),
Text(item.description, style: textTheme.titleMedium),
CategoryChip(
category: category,
),
Text(Unit.fromId(item.unit).display(context, item.value))
]))),
...(item.link != null)
? [
ListTile(
title: Text(AppLocalizations.of(context)!
.itemShowLinkedProductTitle),
subtitle: Text(AppLocalizations.of(context)!
.itemShowLinkedProductSubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// launch "view-product" page for specific product
context.pushNamed('view-product', params: {
'server': server,
'id': room,
'product': item.link.toString()
});
},
)
]
: [],
? [
ListTile(
title: Text(AppLocalizations.of(context)!
.itemShowLinkedProductTitle),
subtitle: Text(AppLocalizations.of(context)!
.itemShowLinkedProductSubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// launch "view-product" page for specific product
context.pushNamed('view-product', params: {
'server': server,
'id': room,
'product': item.link.toString()
});
},
)
]
: [],
...(info != null &&
((info?.isAdmin ?? false) ||
(info?.isOwner ?? false) ||
((info?.permissions)! &
RoomPermission.addShoppingListItems !=
0)))
? [
ListTile(
title: Text(AppLocalizations.of(context)!.editItem),
subtitle: Text(AppLocalizations.of(context)!.editItemLong),
trailing: const Icon(Icons.chevron_right),
onTap: () {
context.pushNamed('edit-product', params: {
'server': server,
'id': room,
'item': item.id.toString()
});
},
),
ListTile(
title:
Text(AppLocalizations.of(context)!.deleteItemTitle),
subtitle: Text(
AppLocalizations.of(context)!.deleteItemSubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// TODO: show confirm dialog
}),
]
: [],
((info?.isAdmin ?? false) ||
(info?.isOwner ?? false) ||
((info?.permissions)! &
RoomPermission.addShoppingListItems !=
0)))
? [
ListTile(
title: Text(AppLocalizations.of(context)!.editItem),
subtitle: Text(AppLocalizations.of(context)!.editItemLong),
trailing: const Icon(Icons.chevron_right),
onTap: () {
context.pushNamed('edit-product', params: {
'server': server,
'id': room,
'item': item.id.toString()
});
},
),
ListTile(
title:
Text(AppLocalizations.of(context)!.deleteItemTitle),
subtitle: Text(
AppLocalizations.of(context)!.deleteItemSubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// TODO: show confirm dialog
}),
]
: [],
ListTile(
title: Text(item.state == 0
? AppLocalizations.of(context)!.moveItemToCartTitle
: AppLocalizations.of(context)!.moveItemToCartSubtitle),
subtitle: Text(item.state == 0
? AppLocalizations.of(context)!.removeItemFromCartTitle
: AppLocalizations.of(context)!.removeItemFromCartSubtitle),
onTap: () {
// flip state
item.state = (item.state - 1).abs();
final user = context.read<User>();
doNetworkRequest(ScaffoldMessenger.of(context),
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'changeItemState',
body: {
'room': room,
'server': server,
'listItemID': item.id,
'state': item.state
}));
})
title: Text(item.state == 0
? AppLocalizations.of(context)!.moveItemToCartTitle
: AppLocalizations.of(context)!.moveItemToCartSubtitle),
subtitle: Text(item.state == 0
? AppLocalizations.of(context)!.removeItemFromCartTitle
: AppLocalizations.of(context)!.removeItemFromCartSubtitle),
onTap: () {
// flip state
item.state = (item.state - 1).abs();
final user = context.read<User>();
doNetworkRequest(ScaffoldMessenger.of(context),
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'changeItemState',
body: {
'room': room,
'server': server,
'listItemID': item.id,
'state': item.state
}));
})
],
),
);

View file

@ -25,24 +25,24 @@ class _RoomProductsPageState extends State<RoomProductsPage> {
final user = context.read<User>();
doNetworkRequest(ScaffoldMessenger.of(context),
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getProducts',
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async {
final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
.toList();
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getProducts',
body: {'room': widget.room?.id, 'server': widget.room?.serverTag}),
onOK: (body) async {
final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
.toList();
// TODO: cache products
// TODO: cache products
if (mounted) {
setState(() {
if (mounted) {
setState(() {
products = resp;
});
}
});
});
}
});
}
@override
@ -73,31 +73,33 @@ class _RoomProductsPageState extends State<RoomProductsPage> {
// where as reading the shopping item description,
// might be a good idea
context.pushNamed('view-product', params: {
'server': widget.room!.serverTag,
'id': widget.room!.id,
'product': item.id.toString()
'server': widget.room!.serverTag,
'id': widget.room!.id,
'product': item.id.toString()
});
},
);
},
),
floatingActionButton: (widget.info != null && ((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)!.newProductShort),
tooltip: AppLocalizations.of(context)!.newProductLong,
onPressed: () {
// show new category popup
context.pushNamed('new-product', params: {
'server': widget.room!.serverTag,
'id': widget.room!.id,
});
},
)
: null,
floatingActionButton: (widget.info != null &&
((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)!.newProductShort),
tooltip: AppLocalizations.of(context)!.newProductLong,
onPressed: () {
// show new category popup
context.pushNamed('new-product', params: {
'server': widget.room!.serverTag,
'id': widget.room!.id,
});
},
)
: null,
);
}
}

View file

@ -15,7 +15,7 @@ class ViewProductPage extends StatefulWidget {
final String server;
final String room;
const ViewProductPage(
{required this.server,
{required this.server,
required this.room,
required this.product,
super.key});
@ -39,14 +39,14 @@ class _ViewProductPageState extends State<ViewProductPage> {
doNetworkRequest(
sm,
req: () => postWithCreadentials(
path: 'getRoomInfo',
credentials: user,
target: user.server,
body: {'room': widget.room, 'server': widget.server}),
path: 'getRoomInfo',
credentials: user,
target: user.server,
body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async {
final info = RoomInfo.fromJSON(body['data']);
setState(() {
this.info = info;
this.info = info;
});
return true;
},
@ -59,25 +59,25 @@ class _ViewProductPageState extends State<ViewProductPage> {
// TODO: load cached categories first
doNetworkRequest(ScaffoldMessenger.of(context),
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getCategories',
body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async {
final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
.toList();
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getCategories',
body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async {
final resp = body['data']
.map<RoomCategory>((raw) => RoomCategory.fromJSON(raw))
.toList();
Map<int?, RoomCategory> map = {};
Map<int?, RoomCategory> map = {};
for (RoomCategory cat in resp) {
map[cat.id] = cat;
}
setState(() {
for (RoomCategory cat in resp) {
map[cat.id] = cat;
}
setState(() {
categories = map;
});
});
});
}
void fetchProducts() {
@ -86,29 +86,29 @@ class _ViewProductPageState extends State<ViewProductPage> {
// TODO: load cached products first
doNetworkRequest(ScaffoldMessenger.of(context),
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getProducts',
body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async {
final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
.toList();
req: () => postWithCreadentials(
credentials: user,
target: user.server,
path: 'getProducts',
body: {'room': widget.room, 'server': widget.server}),
onOK: (body) async {
final resp = body['data']
.map<RoomProduct>((raw) => RoomProduct.fromJSON(raw))
.toList();
for (RoomProduct prod in resp) {
// load product info
// for current product
if (prod.id == widget.product) {
setState(() {
for (RoomProduct prod in resp) {
// load product info
// for current product
if (prod.id == widget.product) {
setState(() {
product = prod;
});
});
}
}
}
setState(() {
setState(() {
products = resp;
});
});
});
}
@override
@ -116,9 +116,9 @@ class _ViewProductPageState extends State<ViewProductPage> {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
fetchCategories();
fetchProducts();
fetchInfo();
fetchCategories();
fetchProducts();
fetchInfo();
});
}
@ -131,10 +131,10 @@ class _ViewProductPageState extends State<ViewProductPage> {
title: Text(product?.name ?? ''),
),
body: SingleChildScrollView(
child: Column(children: [
// display product into
Center(
child: Padding(
child: Column(children: [
// display product into
Center(
child: Padding(
padding: const EdgeInsets.all(14),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@ -142,68 +142,71 @@ class _ViewProductPageState extends State<ViewProductPage> {
children: [
Text(product?.name ?? '', style: textTheme.headlineLarge),
Text(product?.description ?? '',
style: textTheme.titleMedium),
style: textTheme.titleMedium),
Text(product?.ean ?? ''),
CategoryChip(category: categories[product?.category]),
Text(product!=null?Unit.fromId(product!.defaultUnit).display(context, product!.defaultValue):'')
Text(product != null
? Unit.fromId(product!.defaultUnit)
.display(context, product!.defaultValue)
: '')
],
))),
))),
// show actions (if allowed / available
// edit product button
...(info != null &&
(info!.isAdmin ||
info!.isOwner ||
(info!.permissions & RoomPermission.editRoomContent != 0)))
// show actions (if allowed / available
// edit product button
...(info != null &&
(info!.isAdmin ||
info!.isOwner ||
(info!.permissions & RoomPermission.editRoomContent != 0)))
? [
ListTile(
title: Text(AppLocalizations.of(context)!.editProductTitle),
subtitle:
Text(AppLocalizations.of(context)!.editProductSubtitle),
onTap: () {
context.pushNamed('edit-product', params: {
ListTile(
title: Text(AppLocalizations.of(context)!.editProductTitle),
subtitle:
Text(AppLocalizations.of(context)!.editProductSubtitle),
onTap: () {
context.pushNamed('edit-product', params: {
'server': widget.server,
'id': widget.room,
'product': widget.product.toString()
});
},
trailing: const Icon(Icons.chevron_right),
),
]
});
},
trailing: const Icon(Icons.chevron_right),
),
]
: [],
// show parent?
...(product?.parent != null)
// show parent?
...(product?.parent != null)
? [
ListTile(
title: Text(
AppLocalizations.of(context)!.viewParentProductTitle),
subtitle: Text(
AppLocalizations.of(context)!.viewParentProductSubtitle),
onTap: () {
context.pushNamed('view-product', params: {
ListTile(
title: Text(
AppLocalizations.of(context)!.viewParentProductTitle),
subtitle: Text(
AppLocalizations.of(context)!.viewParentProductSubtitle),
onTap: () {
context.pushNamed('view-product', params: {
'server': widget.server,
'id': widget.room,
'product': product!.parent.toString()
});
},
trailing: const Icon(Icons.chevron_right),
),
]
});
},
trailing: const Icon(Icons.chevron_right),
),
]
: [],
// show/manage children
ListTile(
title: Text(AppLocalizations.of(context)!.viewProductChildrenTitle),
subtitle:
// show/manage children
ListTile(
title: Text(AppLocalizations.of(context)!.viewProductChildrenTitle),
subtitle:
Text(AppLocalizations.of(context)!.viewProductChildrenSubtitle),
onTap: () {
context.pushNamed('view-product-children', params: {
'server': widget.server,
'id': widget.room,
'product': widget.product.toString()
});
},
trailing: const Icon(Icons.chevron_right),
),
onTap: () {
context.pushNamed('view-product-children', params: {
'server': widget.server,
'id': widget.room,
'product': widget.product.toString()
});
},
trailing: const Icon(Icons.chevron_right),
),
])),
);
}

View file

@ -0,0 +1 @@

View file

@ -25,54 +25,57 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
title: Text(AppLocalizations.of(context)!.changeThemeTitle),
icon: const Icon(Icons.password),
content: SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: TextField(
controller: _ctrOldPassword,
keyboardType: TextInputType.visiblePassword,
obscureText: true,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock),
labelText: AppLocalizations.of(context)!.inputOldPasswordLabel,
hintText: AppLocalizations.of(context)!.inputOldPasswordHint,
helperText:AppLocalizations.of(context)!.inputOldPasswordHelp,
border: const OutlineInputBorder(),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.all(8),
child: TextField(
controller: _ctrOldPassword,
keyboardType: TextInputType.visiblePassword,
obscureText: true,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock),
labelText: AppLocalizations.of(context)!.inputOldPasswordLabel,
hintText: AppLocalizations.of(context)!.inputOldPasswordHint,
helperText: AppLocalizations.of(context)!.inputOldPasswordHelp,
border: const OutlineInputBorder(),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: TextField(
controller: _ctrNewPassword,
keyboardType: TextInputType.visiblePassword,
obscureText: true,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock),
labelText: AppLocalizations.of(context)!.inputNewPasswordLabel,
hintText: AppLocalizations.of(context)!.inputNewPasswordHint,
helperText:AppLocalizations.of(context)!.inputNewPasswordHelp,
border: const OutlineInputBorder(),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: TextField(
controller: _ctrNewPassword,
keyboardType: TextInputType.visiblePassword,
obscureText: true,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock),
labelText: AppLocalizations.of(context)!.inputNewPasswordLabel,
hintText: AppLocalizations.of(context)!.inputNewPasswordHint,
helperText: AppLocalizations.of(context)!.inputNewPasswordHelp,
border: const OutlineInputBorder(),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: TextField(
controller: _ctrNewPasswordRepeat,
keyboardType: TextInputType.visiblePassword,
obscureText: true,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock),
labelText: AppLocalizations.of(context)!.inputNewPasswordRepeatLabel,
hintText: AppLocalizations.of(context)!.inputNewPasswordRepeatHint,
helperText:AppLocalizations.of(context)!.inputNewPasswordRepeatHelp,
border: const OutlineInputBorder(),
),
),
Padding(
padding: const EdgeInsets.all(8),
child: TextField(
controller: _ctrNewPasswordRepeat,
keyboardType: TextInputType.visiblePassword,
obscureText: true,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock),
labelText:
AppLocalizations.of(context)!.inputNewPasswordRepeatLabel,
hintText:
AppLocalizations.of(context)!.inputNewPasswordRepeatHint,
helperText:
AppLocalizations.of(context)!.inputNewPasswordRepeatHelp,
border: const OutlineInputBorder(),
),
),
],
),
],
)),
actions: [
TextButton(
@ -93,8 +96,7 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
if (_ctrNewPassword.text.length < 6) {
// password has to be at least 6 characters long
showSimpleSnackbar(scaffMgr,
text: trans!.errorPasswordLength,
action: trans.dismiss);
text: trans!.errorPasswordLength, action: trans.dismiss);
_ctrNewPasswordRepeat.clear();
return;
@ -102,7 +104,7 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
if (_ctrNewPassword.text != _ctrNewPasswordRepeat.text) {
// new passwords do not match
showSimpleSnackbar(scaffMgr,
text: trans!.errorPasswordsDoNotMatch, action: trans.dismiss);
text: trans!.errorPasswordsDoNotMatch, action: trans.dismiss);
_ctrNewPasswordRepeat.clear();
return;
@ -110,7 +112,7 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
if (hashPassword(_ctrOldPassword.text) != user.password) {
// current password wrong
showSimpleSnackbar(scaffMgr,
text: trans!.errorOldPasswordWrong, action: trans.dismiss);
text: trans!.errorOldPasswordWrong, action: trans.dismiss);
_ctrOldPassword.clear();
return;
@ -120,23 +122,23 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
// send request
doNetworkRequest(scaffMgr,
req: () => postWithCreadentials(
path: 'changePassword',
target: user.server,
body: {'accountKey': password},
credentials: user),
onOK: (_) async {
// update local user struct
final updatedUser = User(
username: user.username,
password: password,
server: user.server);
await updatedUser.toDisk();
},
after: () {
// close popup
nav.pop();
});
req: () => postWithCreadentials(
path: 'changePassword',
target: user.server,
body: {'accountKey': password},
credentials: user),
onOK: (_) async {
// update local user struct
final updatedUser = User(
username: user.username,
password: password,
server: user.server);
await updatedUser.toDisk();
},
after: () {
// close popup
nav.pop();
});
},
child: Text(AppLocalizations.of(context)!.changeThemeTitle),
)

View file

@ -22,17 +22,17 @@ class _WelcomePageState extends State<WelcomePage> {
_currentPage = controller.initialPage;
controller.addListener(() {
setState(() {
_currentPage = controller.page?.toInt() ?? controller.initialPage;
});
setState(() {
_currentPage = controller.page?.toInt() ?? controller.initialPage;
});
});
}
@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context)
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
String fabText = AppLocalizations.of(context)!.next;
if (_currentPage == 0) {
@ -55,86 +55,80 @@ class _WelcomePageState extends State<WelcomePage> {
body: Column(
children: [
Expanded(
child: PageView(
controller: controller,
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SvgPicture.asset(asset("undraw/undraw_shopping_app.svg"),
child: PageView(
controller: controller,
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SvgPicture.asset(asset("undraw/undraw_shopping_app.svg"),
fit: BoxFit.contain,
width: smallest * 0.5,
height: smallest * 0.5),
Text(
AppLocalizations.of(context)!.welcomeTitle,
style: textTheme.displaySmall,
),
Text(
AppLocalizations.of(context)!.welcomeSubtitle,
style: textTheme.bodyMedium
)
],
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SvgPicture.asset(asset("undraw/undraw_mobile_login.svg"),
fit: BoxFit.contain,
width: smallest * 0.5,
height: smallest * 0.5),
Text(
AppLocalizations.of(context)!.page2Title,
style: textTheme.displaySmall,
),
Text(
AppLocalizations.of(context)!.page2Subtitle,
Text(
AppLocalizations.of(context)!.welcomeTitle,
style: textTheme.displaySmall,
),
Text(AppLocalizations.of(context)!.welcomeSubtitle,
style: textTheme.bodyMedium)
],
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SvgPicture.asset(asset("undraw/undraw_online_connection.svg"),
],
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SvgPicture.asset(asset("undraw/undraw_mobile_login.svg"),
fit: BoxFit.contain,
width: smallest * 0.5,
height: smallest * 0.5),
Text(
AppLocalizations.of(context)!.page3Title,
style: textTheme.displaySmall,
),
Text(
AppLocalizations.of(context)!.page3Subtitle,
Text(
AppLocalizations.of(context)!.page2Title,
style: textTheme.displaySmall,
),
Text(AppLocalizations.of(context)!.page2Subtitle,
style: textTheme.bodyMedium)
],
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SvgPicture.asset(asset("undraw/undraw_online_groceries.svg"),
],
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SvgPicture.asset(asset("undraw/undraw_online_connection.svg"),
fit: BoxFit.contain,
width: smallest * 0.5,
height: smallest * 0.5),
Text(
AppLocalizations.of(context)!.page4Title,
style: textTheme.displaySmall,
),
Text(
AppLocalizations.of(context)!.page4Subtitle,
Text(
AppLocalizations.of(context)!.page3Title,
style: textTheme.displaySmall,
),
Text(AppLocalizations.of(context)!.page3Subtitle,
style: textTheme.bodyMedium)
],
),
],
],
),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
SvgPicture.asset(asset("undraw/undraw_online_groceries.svg"),
fit: BoxFit.contain,
width: smallest * 0.5,
height: smallest * 0.5),
Text(
AppLocalizations.of(context)!.page4Title,
style: textTheme.displaySmall,
),
Text(AppLocalizations.of(context)!.page4Subtitle,
style: textTheme.bodyMedium)
],
),
],
)),
TextButton(
onPressed: () {
context.goNamed('signin');
},
child: Text(AppLocalizations.of(context)!.userHasAnAccount
),
child: Text(AppLocalizations.of(context)!.userHasAnAccount),
)
],
),
@ -149,8 +143,8 @@ class _WelcomePageState extends State<WelcomePage> {
} else {
// move to next page
controller.nextPage(
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 300));
curve: Curves.easeInOut,
duration: const Duration(milliseconds: 300));
}
},
),