Added translations using l10n

Translations are provided in *.arb* format.
Some keys have descriptions
(indicated by leading @-symbol).
Descriptions should not be copied into the translation itself.

Currently only English is supported (app_en.arb),
but German is planned.

Apparently weblate merged .arb support at some time,
so it would be nice to enable community translations at some point.
This commit is contained in:
Jakob Meier 2023-03-29 15:14:27 +02:00
parent 90adcc6bb1
commit 8fffafde47
No known key found for this signature in database
GPG key ID: 66BDC7E6A01A6152
24 changed files with 947 additions and 618 deletions

View file

@ -1,3 +0,0 @@
# Official Outbag App
Source code of the official outbag app,
written in flutter.

15
README.org Normal file
View file

@ -0,0 +1,15 @@
* Official Outbag App
Source code of the official outbag app,
written in flutter.
** Translating
This app uses /l10n/ according to the official flutter
[[https://docs.flutter.dev/development/accessibility-and-localization/internationalization][internationalization guide]].
1. Check if there is a ~.arb~ file available for your language,
and add missing translations in there,
if the file exists
2. Otherwise copy the ~app_en.arb~ file
and paste it as ~arb_<your-language>.arb~
3. Edit the translations in the file
4. Run ~flutter gen-l10n~ to generate the required ~.dart~ files

3
l10n.yaml Normal file
View file

@ -0,0 +1,3 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

View file

@ -1,30 +1,32 @@
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
/* /*
* Tool to automatically generate english text from error * Tool to automatically generate english text from error
*/ */
String errorAsString(Map<String, dynamic> json) { String errorAsString(Map<String, dynamic> json, AppLocalizations trans) {
switch (json['data']) { switch (json['data']) {
case 'notfound': case 'notfound':
return 'Endpoint not found'; return trans.errorNotFound;
case 'wrongstate': case 'wrongstate':
return 'Missing data'; return trans.errorDataIncomplete;
case 'data': case 'data':
return 'Invalid data'; return trans.errorDataInvalid;
case 'roomAdmin': case 'roomAdmin':
case 'right': case 'right':
return 'You are not allowed to perform this action'; return trans.errorPermissions;
case 'server': case 'server':
return 'Server error'; return trans.errorServer;
case 'closed': case 'closed':
return 'Server cannot be reached'; return trans.errorUnreachable;
case 'auth': case 'auth':
return 'Username or password wrong'; return trans.errorAuth;
case 'ota': case 'ota':
return 'Invalid OTA'; return trans.errorInvalidOTA;
case 'existence': case 'existence':
return 'Username unavailable'; return trans.errorUsernameUnavailable;
case 'config': case 'config':
return 'Server reached user limit'; return trans.errorServerLimit;
} }
return "Unknown Error"; return trans.errorUnknown;
} }

View file

@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
// implementation of permission types // implementation of permission types
// according to the server permissions.ts // according to the server permissions.ts
// https://codeberg.org/outbag/server/src/branch/dev/src/server/permissions.ts // https://codeberg.org/outbag/server/src/branch/dev/src/server/permissions.ts
@ -100,45 +102,49 @@ class RoomPermission {
]; ];
} }
static String name(int permission) { static String name(int permission, BuildContext context) {
final trans = AppLocalizations.of(context);
switch (permission) { switch (permission) {
case 1: case 1:
return 'Add Articles'; return trans!.roomPermissionAddArticles;
case 2: case 2:
return 'Remove Articles'; return trans!.roomPermissionRemoveArticles;
case 4: case 4:
return 'List Groups and Items'; return trans!.roomPermissionList;
case 8: case 8:
return 'Change Room Metadata'; return trans!.roomPermissionChangeMeta;
case 16: case 16:
return 'Manage OTAs'; return trans!.roomPermissionManageOTA;
case 32: case 32:
return 'Manage Admins'; return trans!.roomPermissionManageAdmins;
case 64: case 64:
return 'Manage Members'; return trans!.roomPermissionManageMembers;
} }
return "Unknown permission"; return trans!.roomPermissionUnknown;
} }
static String describe(int permission) { static String describe(int permission, BuildContext context) {
final trans = AppLocalizations.of(context);
switch (permission) { switch (permission) {
case 1: case 1:
return 'Allows users to add items to the shopping list'; return trans!.roomPermissionAddArticlesSubtitle;
case 2: case 2:
return 'Allows users to remove items from the shopping list'; return trans!.roomPermissionRemoveArticlesSubtitle;
case 4: case 4:
return 'Allows the user to view groups and products'; return trans!.roomPermissionListSubtitle;
case 8: case 8:
return 'Allows the user to edit the room name, description and icon'; return trans!.roomPermissionChangeMetaSubtitle;
case 16: case 16:
return 'Alloww the user to create, share and delete authentification tokens'; return trans!.roomPermissionManageOTASubtitle;
case 32: case 32:
return 'Allows the user to change the admin status of other members'; return trans!.roomPermissionManageAdminsSubtitle;
case 64: case 64:
return 'Allows the user to invite and kick room members'; return trans!.roomPermissionManageMembersSubtitle;
} }
return "No description available"; return trans!.roomPermissionUnknownSubtitle;
} }
} }

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:localstore/localstore.dart'; import 'package:localstore/localstore.dart';
import 'package:outbag_app/tools/assets.dart'; import 'package:outbag_app/tools/assets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class RoomVisibility { class RoomVisibility {
final int type; final int type;
@ -28,14 +29,16 @@ class RoomVisibility {
return Icons.lock; return Icons.lock;
} }
String get text { String text(BuildContext context) {
final trans = AppLocalizations.of(context);
if (type == 2) { if (type == 2) {
return "Global"; return trans!.roomVisibilityGlobal;
} else if (type == 1) { } else if (type == 1) {
return "Local"; return trans!.roomVisibilityLocal;
} }
return "Private"; return trans!.roomVisibilityPrivate;
} }
static List<RoomVisibility> list() { static List<RoomVisibility> list() {

View file

@ -1,20 +1,23 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:localstore/localstore.dart'; import 'package:localstore/localstore.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class AppTheme { class AppTheme {
ThemeMode mode; ThemeMode mode;
AppTheme(this.mode); AppTheme(this.mode);
String get name { String name(BuildContext context) {
if (mode == ThemeMode.light) { final trans = AppLocalizations.of(context);
return 'Light';
if (mode == ThemeMode.light) {
return trans!.themeLight;
} }
if (mode == ThemeMode.dark) { if (mode == ThemeMode.dark) {
return 'Dark'; return trans!.themeDark;
} }
return 'System'; return trans!.themeSystem;
} }
IconData get icon { IconData get icon {
@ -80,5 +83,4 @@ class AppTheme {
@override @override
int get hashCode => mode.index; int get hashCode => mode.index;
} }

287
lib/l10n/app_en.arb Normal file
View file

@ -0,0 +1,287 @@
{
"helloWorld": "Hello World!",
"@helloWorld": {
"description": "The conventional newborn programmer greeting"
},
"welcomeTitle": "Welcome to Outbag",
"@welcomeTitle": {
"description": "Title shown on welcome screen"
},
"welcomeSubtitle": "Shopping lists made easy",
"@welcomeSubtitle": {
"description": "Subtitle shown on welcome screen"
},
"userHasAnAccount": "I already have an account",
"@userHasAnAccount": {
"description": "Button displayed on welcome screen (bottom-center) used to launch sign in screen"
},
"letsGo": "Let's go",
"@letsGo": {
"description": "Text for button on welcome screen used to move the page viewer the first time"
},
"page2Title": "Open. Decentralized",
"page2Subtitle": "One account, multiple servers",
"next": "Next",
"page3Title": "Made to share",
"page3Subtitle": "Collaborate on your shopping lists in real time",
"continueTour": "Continue Tour",
"takeTour": "Take Tour",
"page4Title": "Pocket-size",
"page4Subtitle": "Always have your shopping lists with you",
"signUp": "Sign Up",
"signIn": "Sign In",
"logIntoAccount": "Log into account",
"createNewAccount": "Create new account",
"inputServerLabel": "Server",
"inputServerHint": "Your homeserver URL",
"inputServerHelp": "Your data will be stored on your homeserver",
"inputUsernameLabel": "Username",
"inputUsernameHint": "Your username",
"inputUsernameHelp": "Your username and server-tag allow others to identify you",
"inputPasswordLabel": "Password",
"inputPasswordHint": "Your password",
"inputPasswordHelp": "Password has to be at least six characters long",
"inputPasswordRepeatLabel": "Repeat Password",
"inputPasswordRepeatHint": "Type your password again",
"inputPasswordRepeatHelp": "Make sure to type the correct password",
"inputOTALabel": "OTA",
"inputOTAHint": "One-Time-Authorization token",
"inputOTAHelp": "This token might be required if the server is rate limited",
"errorPasswordLength": "Password has to be at least six characters long",
"errorPasswordsDoNotMatch": "Passwords do not match",
"errorInvalidServer": "Unable to find outbag server on {server}",
"@errorInvalidServer": {
"description": "Error shown when there is no outbag server on the given url",
"placeholders": {
"server": {
"type": "String",
"example": "outbag.example.com"
}
}
},
"search": "Search",
"settings": "Settings",
"about": "About",
"serverDashboard": "Server Dashboard",
"addRoom": "Add Room",
"addRoomHint": "Add a new room",
"noNewRoomsFound": "No new rooms found",
"joinRoom": "Join Room",
"refresh": "Refresh",
"joinRoomInvite": "Join invite-only room",
"newRoom": "New Room",
"newRoomShort": "New",
"createRoom": "Create Room",
"createRoomShort": "Create",
"changeRoomIcon": "Change room icon",
"chooseRoomIcon": "Choose a room icon",
"inputRoomIdLabel": "Room ID",
"inputRoomIdHint": "Unique room id",
"inputRoomIdHelp": "The room id and server tag allow the room to be identified",
"inputRoomNameLabel": "Room Name",
"inputRoomNameHint": "Give your room a name",
"inputRoomNameHelp": "Choose a human-readable name to easily identify a room",
"inputRoomDescriptionLabel": "Room Description",
"inputRoomDescriptionHint": "Briefly describe your room",
"inputRoomDescriptionHelp": "Make it easier for others to know what this room is used for",
"roomVisibilityTitle": "Visibility",
"roomVisibilitySubtitle": "Specify who has access to your room",
"roomVisibilityPrivate": "Private",
"roomVisibilityLocal": "Local",
"roomVisibilityGlobal": "Global",
"errorNoRoomId": "Please specify a room ID",
"errorRoomIdLength": "Room ID has to be at least three characters long",
"errorNoRoomName": "Please specify a room name",
"errorNetwork": "Network error",
"errorUnknown": "Unknown error",
"errorServer":"Server error",
"errorNotFound":"Not found",
"errorDataIncomplete":"Missing data",
"errorDataInvalid":"Invalid data",
"errorPermissions":"You are not allowed to perform this action",
"errorUnreachable":"Server cannot be reached",
"errorAuth":"Username or password wrong",
"errorInvalidOTA":"Invalid OTA",
"errorUsernameUnavailable":"A user with that name already exists",
"errorServerLimit":"Server reached user limit",
"themeLight": "Light",
"themeDark": "Dark",
"themeSystem": "System",
"roomListTitle": "List",
"roomListSubtitle": "View shopping list",
"roomProductsTitle": "Products",
"roomProductsSubtitle": "View saved items",
"roomCategoriesTitle": "Categories",
"roomCategoriesSubtitle": "View categories",
"roomAboutTitle": "About",
"roomAboutSubtitle": "View room info",
"changeRoomVisibilityTitle": "Change Room Visibility",
"changeRoomVisibilitySubtitle": "Do you really want to change the room visibility to: {visibility}",
"@changeRoomVisibilitySubtitle": {
"placeholders": {
"visibility": {
"type": "String",
"example": "Local"
}
}
},
"editRoomMetadata": "Edit Metadata",
"editRoomMetadataShort": "Edit Room",
"editRoomMetadataSubtitle": "Edit the room name, description and icon",
"showRoomMembers": "Members",
"showRoomMembersSubtitle": "Show Member list",
"editRoomPermissions": "Edit Permissions",
"editRoomPermissionsSubtitle": "Change the default permission-set for all members",
"manageRoomOTA": "OTA",
"manageRoomOTASubtitle": "Add and delete OTAs",
"manageRoomInvites": "Invites",
"manageRoomInvitesSubtitle": "Invite people to this room",
"leaveRoom": "Leave Room",
"leaveRoomShort": "Leave",
"leaveRoomConfirm": "Do you really want to leave this room?",
"deleteRoom": "Delete Room",
"deleteRoomShort": "Delete",
"deleteRoomConfirm": "Do you really want to delete this room?",
"updateRoomPermissions": "Edit",
"updateRoomPermissionsHint": "Update default permission set",
"roomDefaultPermissions": "Default Permissions",
"roomPermissionAddArticles": "Add Articles",
"roomPermissionAddArticlesSubtitle": "Allows users to add items to the shopping list",
"roomPermissionRemoveArticles": "Remove Articles",
"roomPermissionRemoveArticlesSubtitle": "Allows users to remove items from the shopping list",
"roomPermissionList": "List Groups and Items",
"roomPermissionListSubtitle": "Allows the user to view groups and products",
"roomPermissionChangeMeta": "Change Room Metadata",
"roomPermissionChangeMetaSubtitle": "Allows the user to edit the room name, description and icon",
"roomPermissionManageOTA": "Manage OTAs",
"roomPermissionManageOTASubtitle": "Alloww the user to create, share and delete authentification tokens",
"roomPermissionManageAdmins": "Manage Admins",
"roomPermissionManageAdminsSubtitle": "Allows the user to change the admin status of other members",
"roomPermissionManageMembers": "Manage Members",
"roomPermissionManageMembersSubtitle": "Allows the user to invite and kick room members",
"roomPermissionUnknown": "Unknown Permission",
"roomPermissionUnknownSubtitle": "No information available",
"roomMembersTitle": "Room Members ({count})",
"@roomMembersTitle": {
"placeholders": {
"count": {
"type": "int",
"example": "0"
}
}
},
"roleOwner": "Owner",
"roleAdmin": "Admin",
"roleMember": "Member",
"makeAdminTitle": "Make Admin",
"makeAdminSubtitle": "Grants the user the permission to do everything",
"makeAdminConfirm": "Do you really want to make {user} admin?",
"@makeAdminConfirm": {
"placeholders": {
"user": {
"type": "String",
"example": "ash@example.com"
}
}
},
"removeAdminTitle": "Remove admin privileges",
"removeAdminSubtitle": "Revokes admin privileges from the user",
"removeAdminConfirm": "Do you really want to remove {user}'s admin privileges",
"@removeAdminConfirm": {
"placeholders": {
"user": {
"type": "String",
"example": "ash@example.com"
}
}
},
"kickUserTitle": "Kich User",
"kickUserSubtitle": "Temporarily remove user from server (they'll be able to join the room again)",
"kichUserConfirm": "Do you really want to kick {user}?",
"@kickUserConfirm": {
"placeholders": {
"user": {
"type": "String",
"example": "ash@example.com"
}
}
},
"limitRoomCount": "Room count limit",
"limitRoomCountSubtitle": "How many rooms you are allowed to own",
"limitRoomSize": "Room size limit",
"limitRoomSizeSubtitle": "How many items/products/categories each room may contain",
"limitRoomMemberCount": "Room member limit",
"limitRoomMemberCountSubtitle": "How many members each of your rooms may have",
"userDiscoverable": "Discoverable",
"userDiscoverableSubtitle": "Determines if your account can be discovered by users from other servers",
"changeThemeTitle": "Change Theme",
"changeThemeSubtitle": "Choose your preferred color-scheme",
"changePasswordTitle": "Change Password",
"changePasswordSubtitle": "Choose a new password for your account",
"exportAccountTitle": "Export Account",
"exportAccountSubtitle": "Export account data",
"deleteAccountTitle": "Delete Account",
"deleteAccountSubtitle": "Delete your account from your homeserver",
"deleteAccountConfirm": "Do you really want to delete your account?",
"logOut": "Log out",
"logOutConfirm": "Do you really want to log out?",
"inputOldPasswordLabel": "Old Password",
"inputOldPasswordHint": "Your current password",
"inputOldPasswordHelp": "Type your current password here",
"inputNewPasswordLabel": "New Password",
"inputNewPasswordHint": "Your new password",
"inputNewPasswordHelp": "Password has to be at least six characters long",
"inputNewPasswordRepeatLabel": "Repeat new Password",
"inputNewPasswordRepeatHint": "Type your new password again",
"inputNewPasswordRepeatHelp": "Make sure this matches your new password",
"errorPasswordsDontMatch": "New passwords do not match",
"errorOldPasswordWrong": "Your old password is wrong",
"yes": "Yes",
"loading": "Loading",
"dismiss": "Dismiss",
"cancel": "Cancel",
"ok": "OK",
"close": "Close",
"update": "Update"
}

View file

@ -10,6 +10,8 @@ import 'package:outbag_app/screens/room/new.dart';
import 'package:outbag_app/screens/settings/main.dart'; import 'package:outbag_app/screens/settings/main.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import './screens/home.dart'; import './screens/home.dart';
import './screens/welcome.dart'; import './screens/welcome.dart';
import './screens/room/main.dart'; import './screens/room/main.dart';
@ -142,6 +144,13 @@ class _OutbagAppState extends State {
], ],
child: MaterialApp.router( child: MaterialApp.router(
title: "Outbag", title: "Outbag",
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
AppLocalizations.delegate
],
supportedLocales: AppLocalizations.supportedLocales,
themeMode: theme.mode, themeMode: theme.mode,
theme: ThemeData(useMaterial3: true, brightness: Brightness.light), theme: ThemeData(useMaterial3: true, brightness: Brightness.light),
darkTheme: ThemeData(useMaterial3: true, brightness: Brightness.dark), darkTheme: ThemeData(useMaterial3: true, brightness: Brightness.dark),
@ -223,10 +232,6 @@ class _OutbagAppState extends State {
GoRoute( GoRoute(
name: 'room', name: 'room',
path: 'r/:server/:id', path: 'r/:server/:id',
redirect: (context, state) {
print(state.subloc);
return null;
},
builder: (context, state) => RoomPage( builder: (context, state) => RoomPage(
state.params['server'] ?? '', state.params['server'] ?? '',
state.params['id'] ?? ''), state.params['id'] ?? ''),

View file

@ -4,6 +4,7 @@ import 'package:outbag_app/backend/request.dart';
import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:outbag_app/tools/snackbar.dart'; import 'package:outbag_app/tools/snackbar.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../backend/resolve_url.dart'; import '../backend/resolve_url.dart';
enum Mode { enum Mode {
@ -31,13 +32,13 @@ class _AuthPageState extends State<AuthPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
String modeName = "Sign In"; String modeName = AppLocalizations.of(context)!.signIn;
if (widget.mode != Mode.signin) { if (widget.mode != Mode.signin) {
modeName = "Sign Up"; modeName = AppLocalizations.of(context)!.signUp;
} }
String modeDescription = "Log into account"; String modeDescription = AppLocalizations.of(context)!.logIntoAccount;
if (widget.mode != Mode.signin) { if (widget.mode != Mode.signin) {
modeDescription = "Create new account"; modeDescription = AppLocalizations.of(context)!.createNewAccount;
} }
final textTheme = Theme.of(context) final textTheme = Theme.of(context)
@ -52,7 +53,7 @@ class _AuthPageState extends State<AuthPage> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const CircularProgressIndicator(), const CircularProgressIndicator(),
Text('Loading', style: textTheme.titleLarge), Text(AppLocalizations.of(context)!.loading, style: textTheme.titleLarge),
]))) ])))
: Scaffold( : Scaffold(
appBar: AppBar( appBar: AppBar(
@ -70,13 +71,12 @@ class _AuthPageState extends State<AuthPage> {
child: TextField( child: TextField(
controller: _ctrServer, controller: _ctrServer,
keyboardType: TextInputType.url, keyboardType: TextInputType.url,
decoration: const InputDecoration( decoration: InputDecoration(
prefixIcon: Icon(Icons.dns), prefixIcon: const Icon(Icons.dns),
labelText: 'Server', labelText: AppLocalizations.of(context)!.inputServerLabel,
hintText: 'Your homeserver url', hintText: AppLocalizations.of(context)!.inputServerHint,
helperText: helperText:AppLocalizations.of(context)!.inputServerHelp,
'Your data will be stored on your homeserver', border: const OutlineInputBorder(),
border: OutlineInputBorder(),
), ),
), ),
), ),
@ -85,13 +85,12 @@ class _AuthPageState extends State<AuthPage> {
child: TextField( child: TextField(
controller: _ctrUsername, controller: _ctrUsername,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration( decoration: InputDecoration(
prefixIcon: Icon(Icons.person), prefixIcon: const Icon(Icons.person),
labelText: 'Username', labelText: AppLocalizations.of(context)!.inputUsernameLabel,
hintText: 'Your username', hintText: AppLocalizations.of(context)!.inputUsernameHint,
helperText: helperText:AppLocalizations.of(context)!.inputUsernameHelp,
'your username and server tag allow others to identify you', border: const OutlineInputBorder(),
border: OutlineInputBorder(),
), ),
), ),
), ),
@ -101,13 +100,12 @@ class _AuthPageState extends State<AuthPage> {
controller: _ctrPassword, controller: _ctrPassword,
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
obscureText: true, obscureText: true,
decoration: const InputDecoration( decoration: InputDecoration(
prefixIcon: Icon(Icons.lock), prefixIcon: const Icon(Icons.lock),
labelText: 'Password', labelText: AppLocalizations.of(context)!.inputPasswordLabel,
hintText: 'Your password', hintText: AppLocalizations.of(context)!.inputPasswordHint,
helperText: helperText:AppLocalizations.of(context)!.inputPasswordHelp,
'Password have to be at least six characters long', border: const OutlineInputBorder(),
border: OutlineInputBorder(),
), ),
), ),
), ),
@ -120,13 +118,12 @@ class _AuthPageState extends State<AuthPage> {
controller: _ctrPasswordRpt, controller: _ctrPasswordRpt,
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
obscureText: true, obscureText: true,
decoration: const InputDecoration( decoration: InputDecoration(
prefixIcon: Icon(Icons.lock), prefixIcon: const Icon(Icons.lock),
labelText: 'Repeat Password', labelText: AppLocalizations.of(context)!.inputPasswordRepeatLabel,
hintText: 'Type your password again', hintText: AppLocalizations.of(context)!.inputPasswordRepeatHint,
helperText: helperText:AppLocalizations.of(context)!.inputPasswordRepeatHelp,
'Make sure to type the correct password', border: const OutlineInputBorder(),
border: OutlineInputBorder(),
), ),
), ),
) )
@ -140,13 +137,12 @@ class _AuthPageState extends State<AuthPage> {
child: TextField( child: TextField(
controller: _ctrOTA, controller: _ctrOTA,
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
decoration: const InputDecoration( decoration: InputDecoration(
prefixIcon: Icon(Icons.key), prefixIcon: const Icon(Icons.key),
labelText: 'OTA', labelText: AppLocalizations.of(context)!.inputOTALabel,
hintText: 'One-Time-Authorization token', hintText: AppLocalizations.of(context)!.inputOTAHint,
helperText: helperText:AppLocalizations.of(context)!.inputOTAHelp,
'This token might be required if the server is rate limited', border: const OutlineInputBorder(),
border: OutlineInputBorder(),
), ),
), ),
) )
@ -170,7 +166,8 @@ class _AuthPageState extends State<AuthPage> {
}); });
showSimpleSnackbar(scaffMgr, showSimpleSnackbar(scaffMgr,
text: 'Passwords do not match', action: 'Dismiss'); text: AppLocalizations.of(context)!.errorPasswordsDoNotMatch,
action: AppLocalizations.of(context)!.dismiss);
_ctrPasswordRpt.clear(); _ctrPasswordRpt.clear();
return; return;
@ -184,8 +181,8 @@ class _AuthPageState extends State<AuthPage> {
}); });
showSimpleSnackbar(scaffMgr, showSimpleSnackbar(scaffMgr,
text: 'Password has to be at least 6 characters longs', text: AppLocalizations.of(context)!.errorPasswordLength,
action: 'Dismiss'); action: AppLocalizations.of(context)!.dismiss);
_ctrPasswordRpt.clear(); _ctrPasswordRpt.clear();
return; return;
@ -204,9 +201,8 @@ class _AuthPageState extends State<AuthPage> {
}); });
showSimpleSnackbar(scaffMgr, showSimpleSnackbar(scaffMgr,
text: text: AppLocalizations.of(context)!.errorInvalidServer(_ctrServer.text),
'Unable to find valid outbag server on ${_ctrServer.text}', action: AppLocalizations.of(context)!.dismiss);
action: 'Dismiss');
return; return;
} }

View file

@ -6,6 +6,7 @@ import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../backend/room.dart'; import '../backend/room.dart';
class HomePage extends StatefulWidget { class HomePage extends StatefulWidget {
@ -69,7 +70,7 @@ class _HomePageState extends State<HomePage> {
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
tooltip: "Search", tooltip: AppLocalizations.of(context)!.search,
onPressed: () { onPressed: () {
// show searchbar // show searchbar
// NOTE: location currently unknown // NOTE: location currently unknown
@ -91,7 +92,7 @@ class _HomePageState extends State<HomePage> {
menuChildren: [ menuChildren: [
MenuItemButton( MenuItemButton(
leadingIcon: const Icon(Icons.settings), leadingIcon: const Icon(Icons.settings),
child: const Text('Settings'), child: Text(AppLocalizations.of(context)!.settings),
onPressed: () { onPressed: () {
// show settings screen // show settings screen
context.goNamed('settings'); context.goNamed('settings');
@ -103,7 +104,7 @@ class _HomePageState extends State<HomePage> {
? [ ? [
MenuItemButton( MenuItemButton(
leadingIcon: const Icon(Icons.dns), leadingIcon: const Icon(Icons.dns),
child: const Text('Server Dashboard'), child: Text(AppLocalizations.of(context)!.serverDashboard),
onPressed: () { onPressed: () {
// show settings screen // show settings screen
context.goNamed('dash'); context.goNamed('dash');
@ -112,7 +113,7 @@ class _HomePageState extends State<HomePage> {
: [], : [],
MenuItemButton( MenuItemButton(
leadingIcon: const Icon(Icons.info_rounded), leadingIcon: const Icon(Icons.info_rounded),
child: const Text('About'), child: Text(AppLocalizations.of(context)!.about),
onPressed: () { onPressed: () {
// show about screen // show about screen
context.goNamed('about'); context.goNamed('about');
@ -154,13 +155,13 @@ class _HomePageState extends State<HomePage> {
}, },
), ),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: FloatingActionButton.extended(
label: const Text('Add Room'), label: Text(AppLocalizations.of(context)!.addRoom),
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
onPressed: () { onPressed: () {
// create new room // create new room
context.goNamed('add-room'); context.goNamed('add-room');
}, },
tooltip: 'Add new Room', tooltip: AppLocalizations.of(context)!.addRoomHint,
), ),
); );
} }

View file

@ -1,12 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:outbag_app/backend/errors.dart';
import 'package:outbag_app/backend/request.dart'; import 'package:outbag_app/backend/request.dart';
import 'package:outbag_app/backend/room.dart'; import 'package:outbag_app/backend/room.dart';
import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:outbag_app/tools/snackbar.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'dart:math'; import 'dart:math';
class EditRoomPage extends StatefulWidget { class EditRoomPage extends StatefulWidget {
@ -69,7 +70,7 @@ class _EditRoomPageState extends State<EditRoomPage> {
} catch (_) { } catch (_) {
// no room data available // no room data available
// close screen // close screen
router.pushReplacementNamed('homoe'); router.pushReplacementNamed('home');
} }
})(); })();
return true; return true;
@ -77,7 +78,7 @@ class _EditRoomPageState extends State<EditRoomPage> {
onServerErr: (json) { onServerErr: (json) {
// user not allowed to be here // user not allowed to be here
// close screen // close screen
router.pushReplacementNamed('homoe'); router.pushReplacementNamed('home');
return true; return true;
}, },
); );
@ -127,11 +128,11 @@ class _EditRoomPageState extends State<EditRoomPage> {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const CircularProgressIndicator(), const CircularProgressIndicator(),
Text('Loading', style: textTheme.titleLarge), Text(AppLocalizations.of(context)!.loading, style: textTheme.titleLarge),
]))) ])))
: Scaffold( : Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Edit Room'), title: Text(AppLocalizations.of(context)!.editRoomMetadataShort),
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Center( child: Center(
@ -149,13 +150,13 @@ class _EditRoomPageState extends State<EditRoomPage> {
width: smallest * 0.3, width: smallest * 0.3,
height: smallest * 0.3, height: smallest * 0.3,
), ),
tooltip: 'Change room icon', tooltip: AppLocalizations.of(context)!.changeRoomIcon,
onPressed: () { onPressed: () {
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: const Text( title: Text(
'Choose a room Icon'), AppLocalizations.of(context)!.chooseRoomIcon),
actions: const [], actions: const [],
content: SizedBox( content: SizedBox(
width: smallest * 0.3 * 3, width: smallest * 0.3 * 3,
@ -195,13 +196,15 @@ class _EditRoomPageState extends State<EditRoomPage> {
enabled: false, enabled: false,
controller: _ctrID, controller: _ctrID,
keyboardType: TextInputType.emailAddress, keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration( decoration: InputDecoration(
prefixIcon: Icon(Icons.fact_check), prefixIcon: const Icon(Icons.fact_check),
labelText: 'Room ID', labelText: AppLocalizations.of(context)!
hintText: 'Unique room id', .inputRoomIdLabel,
helperText: hintText: AppLocalizations.of(context)!
'the room id and server tag allow the room to be identified', .inputRoomIdHint,
border: OutlineInputBorder(), helperText: AppLocalizations.of(context)!
.inputRoomIdHelp,
border: const OutlineInputBorder(),
), ),
), ),
), ),
@ -210,13 +213,15 @@ class _EditRoomPageState extends State<EditRoomPage> {
child: TextField( child: TextField(
controller: _ctrName, controller: _ctrName,
keyboardType: TextInputType.name, keyboardType: TextInputType.name,
decoration: const InputDecoration( decoration: InputDecoration(
prefixIcon: Icon(Icons.badge), prefixIcon: const Icon(Icons.badge),
labelText: 'Room Name', labelText: AppLocalizations.of(context)!
hintText: 'Give your room a name', .inputRoomNameLabel,
helperText: hintText: AppLocalizations.of(context)!
'Easily identify a room with a human readable name', .inputRoomNameHint,
border: OutlineInputBorder(), helperText: AppLocalizations.of(context)!
.inputRoomNameHelp,
border: const OutlineInputBorder(),
), ),
), ),
), ),
@ -225,13 +230,15 @@ class _EditRoomPageState extends State<EditRoomPage> {
child: TextField( child: TextField(
controller: _ctrDescription, controller: _ctrDescription,
keyboardType: TextInputType.text, keyboardType: TextInputType.text,
decoration: const InputDecoration( decoration: InputDecoration(
prefixIcon: Icon(Icons.dns), labelText: AppLocalizations.of(context)!
labelText: 'Room Description', .inputRoomDescriptionLabel,
hintText: 'Briefly describe your Room', hintText: AppLocalizations.of(context)!
helperText: .inputRoomDescriptionHint,
'Make it easier for other to know what this room is used for', helperText: AppLocalizations.of(context)!
border: OutlineInputBorder(), .inputRoomDescriptionHelp,
prefixIcon: const Icon(Icons.dns),
border: const OutlineInputBorder(),
), ),
), ),
), ),
@ -241,22 +248,12 @@ class _EditRoomPageState extends State<EditRoomPage> {
onPressed: () async { onPressed: () async {
final scaffMgr = ScaffoldMessenger.of(context); final scaffMgr = ScaffoldMessenger.of(context);
final nav = Navigator.of(context); final nav = Navigator.of(context);
final trans = AppLocalizations.of(context);
// name may not be empty // name may not be empty
if (_ctrName.text.isEmpty) { if (_ctrName.text.isEmpty) {
final snackBar = SnackBar( showSimpleSnackbar(scaffMgr,
behavior: SnackBarBehavior.floating, text: trans!.errorNoRoomName, action: trans.ok);
content: const Text('Please specify a room name'),
action: SnackBarAction(
label: 'Ok',
onPressed: () {
scaffMgr.hideCurrentSnackBar();
},
),
);
scaffMgr.hideCurrentSnackBar();
scaffMgr.showSnackBar(snackBar);
return; return;
} }
@ -275,8 +272,8 @@ class _EditRoomPageState extends State<EditRoomPage> {
clone.description = _ctrDescription.text; clone.description = _ctrDescription.text;
clone.icon = _ctrIcon; clone.icon = _ctrIcon;
try { doNetworkRequest(scaffMgr,
final resp = await postWithCreadentials( req: ()=>postWithCreadentials(
target: user.server, target: user.server,
credentials: user, credentials: user,
path: 'changeRoomMeta', path: 'changeRoomMeta',
@ -286,45 +283,16 @@ class _EditRoomPageState extends State<EditRoomPage> {
'title': clone.name, 'title': clone.name,
'description': clone.description, 'description': clone.description,
'icon': clone.icon?.type, 'icon': clone.icon?.type,
}); }),
if (resp.res == Result.ok) { onOK: (_) async {
// room was created // room was created
// save room // save room
await clone.toDisk(); await clone.toDisk();
nav.pop(); nav.pop();
} else {
// error
final snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: Text(errorAsString(resp.body)),
action: SnackBarAction(
label: 'Dismiss',
onPressed: () {
scaffMgr.hideCurrentSnackBar();
},
),
);
scaffMgr.hideCurrentSnackBar();
scaffMgr.showSnackBar(snackBar);
} }
} catch (_) { );
final snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: const Text('Network error'),
action: SnackBarAction(
label: 'Dismiss',
onPressed: () {
scaffMgr.hideCurrentSnackBar();
},
),
);
scaffMgr.hideCurrentSnackBar();
scaffMgr.showSnackBar(snackBar);
}
}, },
label: const Text('Update'), label: Text(AppLocalizations.of(context)!.update),
icon: const Icon(Icons.edit)), icon: const Icon(Icons.edit)),
); );
} }

View file

@ -6,6 +6,7 @@ import 'package:outbag_app/backend/room.dart';
import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'dart:math'; import 'dart:math';
class JoinRoomPage extends StatefulWidget { class JoinRoomPage extends StatefulWidget {
@ -102,7 +103,7 @@ class _JoinRoomPageState extends State {
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Icons.search), icon: const Icon(Icons.search),
tooltip: "Search", tooltip: AppLocalizations.of(context)!.search,
onPressed: () { onPressed: () {
// show searchbar // show searchbar
// NOTE: location currently unknown // NOTE: location currently unknown
@ -110,7 +111,7 @@ class _JoinRoomPageState extends State {
), ),
IconButton( IconButton(
icon: const Icon(Icons.refresh), icon: const Icon(Icons.refresh),
tooltip: "Refresh", tooltip: AppLocalizations.of(context)!.refresh,
onPressed: () { onPressed: () {
// fetch public rooms again // fetch public rooms again
didChangeDependencies(); didChangeDependencies();
@ -132,7 +133,7 @@ class _JoinRoomPageState extends State {
menuChildren: [ menuChildren: [
MenuItemButton( MenuItemButton(
leadingIcon: const Icon(Icons.drafts), leadingIcon: const Icon(Icons.drafts),
child: const Text('Join invite-only room'), child: Text(AppLocalizations.of(context)!.joinRoomInvite),
onPressed: () { onPressed: () {
// show settings screen // show settings screen
context.goNamed('join-room-ota'); context.goNamed('join-room-ota');
@ -146,7 +147,7 @@ class _JoinRoomPageState extends State {
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
Text('No new Rooms found', style: textTheme.titleLarge), Text(AppLocalizations.of(context)!.noNewRoomsFound, style: textTheme.titleLarge),
], ],
)) ))
: ListView.builder( : ListView.builder(
@ -159,7 +160,7 @@ class _JoinRoomPageState extends State {
semanticContainer: true, semanticContainer: true,
child: InkWell( child: InkWell(
onTap: () { onTap: () {
// TODO: show modalBottomSheet // show modalBottomSheet
// with room information // with room information
// and join button // and join button
showModalBottomSheet( showModalBottomSheet(
@ -202,7 +203,7 @@ class _JoinRoomPageState extends State {
children: [ children: [
Icon(room.visibility?.icon), Icon(room.visibility?.icon),
Text((room Text((room
.visibility?.text)!), .visibility?.text(context))!),
]), ]),
])), ])),
// action buttons // action buttons
@ -217,7 +218,7 @@ class _JoinRoomPageState extends State {
child: ElevatedButton.icon( child: ElevatedButton.icon(
icon: icon:
const Icon(Icons.close), const Icon(Icons.close),
label: const Text('Cancel'), label: Text(AppLocalizations.of(context)!.cancel),
onPressed: () { onPressed: () {
// close sheet // close sheet
Navigator.pop(context); Navigator.pop(context);
@ -230,7 +231,7 @@ class _JoinRoomPageState extends State {
child: FilledButton.icon( child: FilledButton.icon(
icon: icon:
const Icon(Icons.check), const Icon(Icons.check),
label: const Text('Join'), label: Text(AppLocalizations.of(context)!.joinRoom),
onPressed: () async { onPressed: () async {
final scaffMgr = final scaffMgr =
ScaffoldMessenger.of( ScaffoldMessenger.of(
@ -294,13 +295,13 @@ class _JoinRoomPageState extends State {
}, },
), ),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: FloatingActionButton.extended(
label: const Text('New'), label: Text(AppLocalizations.of(context)!.newRoom),
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
onPressed: () { onPressed: () {
// create new room // create new room
context.goNamed('new-room'); context.goNamed('new-room');
}, },
tooltip: 'Create Room', tooltip: AppLocalizations.of(context)!.createRoomShort,
), ),
); );
} }

View file

@ -8,6 +8,7 @@ import 'package:outbag_app/screens/room/pages/products.dart';
import 'package:outbag_app/screens/room/pages/list.dart'; import 'package:outbag_app/screens/room/pages/list.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class RoomPage extends StatefulWidget { class RoomPage extends StatefulWidget {
final String server; final String server;
@ -121,23 +122,27 @@ class _RoomPageState extends State<RoomPage> {
duration: const Duration(milliseconds: 300)); duration: const Duration(milliseconds: 300));
}, },
selectedIndex: page, selectedIndex: page,
destinations: const [ destinations: [
NavigationDestination( NavigationDestination(
icon: Icon(Icons.list), icon: const Icon(Icons.list),
label: "List", label: AppLocalizations.of(context)!.roomListTitle,
tooltip: 'View shopping list'), tooltip: AppLocalizations.of(context)!.roomListSubtitle
),
NavigationDestination( NavigationDestination(
icon: Icon(Icons.inventory_2), icon: const Icon(Icons.inventory_2),
label: "Products", label: AppLocalizations.of(context)!.roomProductsTitle,
tooltip: 'View saved items'), tooltip: AppLocalizations.of(context)!.roomProductsSubtitle
),
NavigationDestination( NavigationDestination(
icon: Icon(Icons.category), icon: const Icon(Icons.category),
label: "Categories", label: AppLocalizations.of(context)!.roomCategoriesTitle,
tooltip: 'View categories'), tooltip: AppLocalizations.of(context)!.roomCategoriesSubtitle
),
NavigationDestination( NavigationDestination(
icon: Icon(Icons.info_rounded), icon: const Icon(Icons.info_rounded),
label: "About", label: AppLocalizations.of(context)!.roomAboutTitle,
tooltip: 'View room info'), tooltip: AppLocalizations.of(context)!.roomAboutSubtitle
),
], ],
), ),
); );

View file

@ -6,6 +6,7 @@ import 'package:outbag_app/backend/room.dart';
import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class ManageRoomMembersPage extends StatefulWidget { class ManageRoomMembersPage extends StatefulWidget {
final String server; final String server;
@ -95,15 +96,8 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Room Members (${list.length})'), title: Text(
leading: IconButton( AppLocalizations.of(context)!.roomMembersTitle(list.length)),
onPressed: () {
// go back
Navigator.of(context).pop();
},
icon: const Icon(Icons.arrow_back),
tooltip: "Go back",
),
//actions: [ //actions: [
// // NOTE: Maybe add a search icon // // NOTE: Maybe add a search icon
// // and general search functionality here // // and general search functionality here
@ -113,13 +107,13 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
final item = list[index]; final item = list[index];
String role = "Member"; String role = AppLocalizations.of(context)!.roleMember;
if (info != null && if (info != null &&
(info?.owner)! == item.id && (info?.owner)! == item.id &&
widget.server == item.serverTag) { widget.server == item.serverTag) {
role = "Owner"; role = AppLocalizations.of(context)!.roleOwner;
} else if (item.isAdmin) { } else if (item.isAdmin) {
role = "Admin"; role = AppLocalizations.of(context)!.roleAdmin;
} }
bool enable = true; bool enable = true;
@ -172,11 +166,11 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
leading: const Icon( leading: const Icon(
Icons.supervisor_account), Icons.supervisor_account),
title: Text(item.isAdmin title: Text(item.isAdmin
? 'Remove admin privileges' ? AppLocalizations.of(context)!.removeAdminTitle
: 'Make Admin'), : AppLocalizations.of(context)!.makeAdminTitle),
subtitle: Text(item.isAdmin subtitle: Text(item.isAdmin
? 'Revokes admin privileges from the user' ? AppLocalizations.of(context)!.removeAdminSubtitle
: 'Grants the user the permission to do everything'), : AppLocalizations.of(context)!.makeAdminSubtitle),
onTap: () { onTap: () {
// make user admin // make user admin
showDialog( showDialog(
@ -189,12 +183,12 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
.supervisor_account), .supervisor_account),
title: Text(item title: Text(item
.isAdmin .isAdmin
? 'Remove admin privileges' ? AppLocalizations.of(context)!.removeAdminTitle
: 'Make Admin'), : AppLocalizations.of(context)!.makeAdminTitle),
content: Text(item content: Text(item
.isAdmin .isAdmin
? "Do you really want to remove ${item.humanReadableName}'s admin privileges" ? AppLocalizations.of(context)!.removeAdminConfirm(item.humanReadableName)
: 'Do you really want to make ${item.humanReadableName} admin?'), : AppLocalizations.of(context)!.makeAdminConfirm(item.humanReadableName)),
actions: [ actions: [
TextButton( TextButton(
onPressed: onPressed:
@ -205,8 +199,7 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
Navigator.of(context) Navigator.of(context)
.pop(); .pop();
}, },
child: const Text( child: Text(AppLocalizations.of(context)!.cancel),
'Cancel'),
), ),
FilledButton( FilledButton(
onPressed: onPressed:
@ -238,8 +231,7 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
nav.pop(); nav.pop();
}); });
}, },
child: const Text( child: Text(AppLocalizations.of(context)!.ok),
'OK'),
) )
], ],
)); ));
@ -257,10 +249,8 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
ListTile( ListTile(
leading: const Icon( leading: const Icon(
Icons.person_remove), Icons.person_remove),
title: title: Text(AppLocalizations.of(context)!.kickUserTitle),
const Text('Kick User'), subtitle: Text(AppLocalizations.of(context)!.kickUserSubtitle),
subtitle: const Text(
"Temporarrily remove user from server (they'll be able to join the room again)"),
onTap: () { onTap: () {
// remove user from room // remove user from room
showDialog( showDialog(
@ -271,10 +261,10 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
icon: const Icon( icon: const Icon(
Icons Icons
.person_remove), .person_remove),
title: const Text( title: Text(
'Kick User'), AppLocalizations.of(context)!.kickUserTitle),
content: Text( content: Text(
'Do you really want to kick ${item.humanReadableName}?'), AppLocalizations.of(context)!.kichUserConfirm(item.humanReadableName)),
actions: [ actions: [
TextButton( TextButton(
onPressed: onPressed:
@ -286,8 +276,8 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
Navigator.of(ctx) Navigator.of(ctx)
.pop(); .pop();
}, },
child: const Text( child: Text(
'Cancel'), AppLocalizations.of(context)!.cancel),
), ),
FilledButton( FilledButton(
onPressed: onPressed:
@ -318,8 +308,8 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
nav.pop(); nav.pop();
}); });
}, },
child: const Text( child: Text(
'Kick User'), AppLocalizations.of(context)!.ok),
) )
], ],
)); ));
@ -331,7 +321,7 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
), ),
), ),
FilledButton( FilledButton(
child: const Text('Close'), child: Text(AppLocalizations.of(context)!.close),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },

View file

@ -7,6 +7,7 @@ import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:outbag_app/tools/snackbar.dart'; import 'package:outbag_app/tools/snackbar.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'dart:math'; import 'dart:math';
class NewRoomPage extends StatefulWidget { class NewRoomPage extends StatefulWidget {
@ -28,208 +29,226 @@ class _NewRoomPageState extends State {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final textTheme = Theme.of(context) final textTheme = Theme.of(context)
.textTheme .textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface); .apply(displayColor: Theme.of(context).colorScheme.onSurface);
double width = MediaQuery.of(context).size.width; double width = MediaQuery.of(context).size.width;
double height = MediaQuery.of(context).size.height; double height = MediaQuery.of(context).size.height;
double smallest = min(min(width, height), 400); double smallest = min(min(width, height), 400);
return showSpinner return showSpinner
? Scaffold( ? Scaffold(
body: Center( body: Center(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const CircularProgressIndicator(), const CircularProgressIndicator(),
Text('Loading', style: textTheme.titleLarge), Text(AppLocalizations.of(context)!.loading,
]))) style: textTheme.titleLarge),
: Scaffold( ])))
appBar: AppBar( : Scaffold(
title: const Text('New Room'), appBar: AppBar(
), title: Text(AppLocalizations.of(context)!.newRoom),
body: SingleChildScrollView( ),
child: Center( body: SingleChildScrollView(
child: Padding( child: Center(
padding: const EdgeInsets.all(14), child: Padding(
child: ConstrainedBox( padding: const EdgeInsets.all(14),
constraints: const BoxConstraints(maxWidth: 400), child: ConstrainedBox(
child: Column( constraints: const BoxConstraints(maxWidth: 400),
mainAxisAlignment: MainAxisAlignment.center, child: Column(
crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ crossAxisAlignment: CrossAxisAlignment.center,
IconButton( children: [
icon: SvgPicture.asset( IconButton(
_ctrIcon.img, icon: SvgPicture.asset(
width: smallest * 0.3, _ctrIcon.img,
height: smallest * 0.3, width: smallest * 0.3,
), height: smallest * 0.3,
tooltip: 'Change room icon', ),
onPressed: () { tooltip: AppLocalizations.of(context)!
showDialog( .changeRoomIcon,
context: context, onPressed: () {
builder: (ctx) => AlertDialog( showDialog(
title: const Text( context: context,
'Choose a room Icon'), builder: (ctx) => AlertDialog(
actions: const [], title: Text(
content: SizedBox( AppLocalizations.of(context)!
width: smallest * 0.3 * 3, .chooseRoomIcon),
height: smallest * 0.3 * 3, actions: const [],
child: GridView.count( content: SizedBox(
crossAxisCount: 3, width: smallest * 0.3 * 3,
children: RoomIcon.list() height: smallest * 0.3 * 3,
.map((icon) { child: GridView.count(
return GridTile( crossAxisCount: 3,
child: IconButton( children: RoomIcon.list()
icon: SvgPicture .map((icon) {
.asset( return GridTile(
icon.img, child: IconButton(
width: icon: SvgPicture
smallest * .asset(
0.3, icon.img,
height: width:
smallest * smallest *
0.3, 0.3,
), height:
tooltip: smallest *
icon.text, 0.3,
onPressed: () { ),
setState(() { // do not display tooltip for now
_ctrIcon = // as it is hard to translate
icon; // and the tooltip prevented the click event,
}); // when clicked on the tooltip bar
Navigator.of( // tooltip:icon.text,
context) onPressed: () {
.pop(); setState(() {
})); _ctrIcon =
}).toList())), icon;
)); });
}, Navigator.of(
), context)
Padding( .pop();
padding: const EdgeInsets.all(8), }));
child: TextField( }).toList())),
controller: _ctrID, ));
keyboardType: TextInputType.emailAddress, },
decoration: const InputDecoration( ),
prefixIcon: Icon(Icons.fact_check), Padding(
labelText: 'Room ID', padding: const EdgeInsets.all(8),
hintText: 'Unique room id', child: TextField(
helperText: controller: _ctrID,
'the room id and server tag allow the room to be identified', keyboardType: TextInputType.emailAddress,
border: OutlineInputBorder(), decoration: InputDecoration(
), prefixIcon: const Icon(Icons.fact_check),
), labelText: AppLocalizations.of(context)!
), .inputRoomIdLabel,
Padding( hintText: AppLocalizations.of(context)!
padding: const EdgeInsets.all(8), .inputRoomIdHint,
child: TextField( helperText: AppLocalizations.of(context)!
controller: _ctrName, .inputRoomIdHelp,
keyboardType: TextInputType.name, border: const OutlineInputBorder(),
decoration: const InputDecoration( ),
prefixIcon: Icon(Icons.badge), ),
labelText: 'Room Name', ),
hintText: 'Give your room a name', Padding(
helperText: padding: const EdgeInsets.all(8),
'Easily identify a room with a human readable name', child: TextField(
border: OutlineInputBorder(), controller: _ctrName,
), keyboardType: TextInputType.name,
), decoration: InputDecoration(
), prefixIcon: const Icon(Icons.badge),
Padding( labelText: AppLocalizations.of(context)!
padding: const EdgeInsets.all(8), .inputRoomNameLabel,
child: TextField( hintText: AppLocalizations.of(context)!
controller: _ctrDescription, .inputRoomNameHint,
keyboardType: TextInputType.text, helperText: AppLocalizations.of(context)!
decoration: const InputDecoration( .inputRoomNameHelp,
prefixIcon: Icon(Icons.dns), border: const OutlineInputBorder(),
labelText: 'Room Description', ),
hintText: 'Briefly describe your Room', ),
helperText: ),
'Make it easier for other to know what this room is used for', Padding(
border: OutlineInputBorder(), padding: const EdgeInsets.all(8),
), child: TextField(
), controller: _ctrDescription,
), keyboardType: TextInputType.text,
Text('Visibility', style: textTheme.labelLarge), decoration: InputDecoration(
Text('Specify who has access to your room', labelText: AppLocalizations.of(context)!
style: textTheme.bodySmall), .inputRoomDescriptionLabel,
SegmentedButton<RoomVisibility>( hintText: AppLocalizations.of(context)!
showSelectedIcon: true, .inputRoomDescriptionHint,
multiSelectionEnabled: false, helperText: AppLocalizations.of(context)!
emptySelectionAllowed: false, .inputRoomDescriptionHelp,
segments: RoomVisibility.list().map((vis) { prefixIcon: const Icon(Icons.dns),
return ButtonSegment<RoomVisibility>( border: const OutlineInputBorder(),
value: vis, ),
label: Text(vis.text), ),
icon: Icon(vis.icon)); ),
}).toList(), Text(
onSelectionChanged: ((vset) { AppLocalizations.of(context)!
setState(() { .roomVisibilityTitle,
_ctrVis = vset.single; style: textTheme.labelLarge),
}); Text(
}), AppLocalizations.of(context)!
selected: {_ctrVis}, .roomVisibilitySubtitle,
selectedIcon: Icon(_ctrVis.icon), style: textTheme.bodySmall),
), SegmentedButton<RoomVisibility>(
], showSelectedIcon: true,
))))), multiSelectionEnabled: false,
floatingActionButton: FloatingActionButton.extended( emptySelectionAllowed: false,
onPressed: () async { segments: RoomVisibility.list().map((vis) {
final scaffMgr = ScaffoldMessenger.of(context); return ButtonSegment<RoomVisibility>(
final router = GoRouter.of(context); 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);
// ID should be at least three characters long // ID should be at least three characters long
if (_ctrID.text.length < 3) { if (_ctrID.text.length < 3) {
showSimpleSnackbar(scaffMgr, showSimpleSnackbar(scaffMgr,
text: _ctrID.text.isEmpty text: _ctrID.text.isEmpty
? 'Please specify a Room ID' ? trans!.errorNoRoomId
: 'Room ID has to be at least three characters long', : trans!.errorRoomIdLength,
action: 'OK'); action: trans.ok);
return; return;
} }
// name may not be empty // name may not be empty
if (_ctrName.text.isEmpty) { if (_ctrName.text.isEmpty) {
showSimpleSnackbar(scaffMgr, showSimpleSnackbar(scaffMgr,
text: 'Please specify a room name', action: 'OK'); text: trans!.errorNoRoomName, action: trans.ok);
return; return;
} }
final user = context.read<User>(); final user = context.read<User>();
final room = Room( final room = Room(
id: _ctrID.text, id: _ctrID.text,
serverTag: user.server.tag, serverTag: user.server.tag,
name: _ctrName.text, name: _ctrName.text,
description: _ctrDescription.text, description: _ctrDescription.text,
icon: _ctrIcon, icon: _ctrIcon,
visibility: _ctrVis); visibility: _ctrVis);
doNetworkRequest(scaffMgr, doNetworkRequest(scaffMgr,
req: () => postWithCreadentials( req: () => postWithCreadentials(
target: user.server, target: user.server,
credentials: user, credentials: user,
path: 'createRoom', path: 'createRoom',
body: { body: {
'room': room.id, 'room': room.id,
'title': room.name, 'title': room.name,
'description': room.description, 'description': room.description,
'icon': room.icon?.type, 'icon': room.icon?.type,
'visibility': room.visibility?.type 'visibility': room.visibility?.type
}), }),
onOK: (_) async { onOK: (_) async {
// room was created // room was created
// save room // save room
await room.toDisk(); await room.toDisk();
// move to home page // move to home page
router.pushReplacementNamed('home'); router.pushReplacementNamed('home');
}); });
}, },
label: const Text('Create'), label: Text(AppLocalizations.of(context)!.createRoomShort),
icon: const Icon(Icons.add)), icon: const Icon(Icons.add)),
); );
} }
} }

View file

@ -8,6 +8,7 @@ import 'package:outbag_app/backend/user.dart';
import 'dart:math'; import 'dart:math';
import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class AboutRoomPage extends StatefulWidget { class AboutRoomPage extends StatefulWidget {
final RoomInfo? info; final RoomInfo? info;
@ -67,7 +68,7 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
segments: RoomVisibility.list().map((vis) { segments: RoomVisibility.list().map((vis) {
return ButtonSegment<int>( return ButtonSegment<int>(
value: vis.type, value: vis.type,
label: Text(vis.text), label: Text(vis.text(context)),
icon: Icon(vis.icon)); icon: Icon(vis.icon));
}).toList(), }).toList(),
onSelectionChanged: ((vset) { onSelectionChanged: ((vset) {
@ -90,16 +91,14 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: title: Text(AppLocalizations.of(context)!.changeRoomVisibilityTitle),
const Text('Change room visibility'), content: Text(AppLocalizations.of(context)!.changeRoomVisibilitySubtitle(vis.text(context))),
content: Text(
'Do you really want to change the room visibility to: ${vis.text}'),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
context.pop(); context.pop();
}, },
child: const Text('Cancel'), child: Text(AppLocalizations.of(context)!.cancel),
), ),
FilledButton( FilledButton(
onPressed: () async { onPressed: () async {
@ -128,7 +127,7 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
nav.pop(); nav.pop();
}); });
}, },
child: const Text('Ok'), child: Text(AppLocalizations.of(context)!.ok),
) )
], ],
)); ));
@ -156,9 +155,8 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
? [ ? [
ListTile( ListTile(
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
title: const Text('Edit Metadata'), title: Text(AppLocalizations.of(context)!.editRoomMetadata),
subtitle: const Text( subtitle: Text(AppLocalizations.of(context)!.editRoomMetadataSubtitle),
'Change the rooms name, description and icon'),
onTap: () { onTap: () {
// show edit room screen // show edit room screen
context.goNamed('edit-room', params: { context.goNamed('edit-room', params: {
@ -172,8 +170,8 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
// open members view // open members view
ListTile( ListTile(
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
title: const Text('Members'), title: Text(AppLocalizations.of(context)!.showRoomMembers),
subtitle: const Text('Show Member list'), subtitle: Text(AppLocalizations.of(context)!.showRoomMembersSubtitle),
onTap: () { onTap: () {
// open member view screen // open member view screen
context.goNamed('room-members', params: { context.goNamed('room-members', params: {
@ -192,9 +190,8 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
? [ ? [
ListTile( ListTile(
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
title: const Text('Edit Permissions'), title: Text(AppLocalizations.of(context)!.editRoomPermissions),
subtitle: const Text( subtitle: Text(AppLocalizations.of(context)!.editRoomPermissionsSubtitle),
'Change the default permission-set for all members'),
onTap: () { onTap: () {
// show checkbox screen // show checkbox screen
context.goNamed('room-permissions', params: { context.goNamed('room-permissions', params: {
@ -213,8 +210,8 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
? [ ? [
ListTile( ListTile(
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
title: const Text('OTA'), title: Text(AppLocalizations.of(context)!.manageRoomOTA),
subtitle: const Text('Add and delete OTAs'), subtitle: Text(AppLocalizations.of(context)!.manageRoomOTASubtitle),
onTap: () { onTap: () {
// show manage ota screen // show manage ota screen
context.goNamed('room-ota', params: { context.goNamed('room-ota', params: {
@ -225,8 +222,8 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
), ),
ListTile( ListTile(
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
title: const Text('Invites'), title: Text(AppLocalizations.of(context)!.manageRoomInvites),
subtitle: const Text('Invite people to this room'), subtitle: Text(AppLocalizations.of(context)!.manageRoomInvitesSubtitle),
onTap: () { onTap: () {
// show manage ota screen // show manage ota screen
context.goNamed('room-invite', params: { context.goNamed('room-invite', params: {
@ -246,25 +243,26 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: FilledButton.tonal( child: FilledButton.tonal(
child: Text(((widget.info?.isOwner)!) child: Text(((widget.info?.isOwner)!)
? 'Delete Room' ? AppLocalizations.of(context)!.deleteRoom
: 'Leave Room'), : AppLocalizations.of(context)!.leaveRoom),
onPressed: () { onPressed: () {
// show confirm dialog // show confirm dialog
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: Text(((widget.info?.isOwner)!) title: Text(((widget.info?.isOwner)!)
? 'Delete Room' ? AppLocalizations.of(context)!.deleteRoom
: 'Leave Room'), : AppLocalizations.of(context)!.leaveRoom),
content: Text( content: Text(((widget.info?.isOwner)!)
'Do you really want to ${((widget.info?.isOwner)!) ? "delete" : "leave"} the room?'), ? AppLocalizations.of(context)!.deleteRoomConfirm
: AppLocalizations.of(context)!.leaveRoomConfirm),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
// close popup // close popup
Navigator.of(ctx).pop(); Navigator.of(ctx).pop();
}, },
child: const Text('Cancel'), child: Text(AppLocalizations.of(context)!.cancel),
), ),
FilledButton( FilledButton(
onPressed: () async { onPressed: () async {
@ -302,8 +300,8 @@ class _AboutRoomPageState extends State<AboutRoomPage> {
}); });
}, },
child: Text(((widget.info?.isOwner)!) child: Text(((widget.info?.isOwner)!)
? 'Delete' ? AppLocalizations.of(context)!.deleteRoomShort
: 'Leave'), : AppLocalizations.of(context)!.leaveRoomShort),
) )
], ],
)); ));

View file

@ -1,5 +1,5 @@
import 'dart:math'; import 'dart:math';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:outbag_app/backend/permissions.dart'; import 'package:outbag_app/backend/permissions.dart';
@ -31,10 +31,10 @@ class _EditRoomPermissionSetPageState extends State<EditRoomPermissionSetPage> {
doNetworkRequest( doNetworkRequest(
sm, sm,
req: () => postWithCreadentials( req: () => postWithCreadentials(
path: 'getRoomInfo', path: 'getRoomInfo',
credentials: user, credentials: user,
target: user.server, target: user.server,
body: {'room': widget.tag, 'server': widget.server}), body: {'room': widget.tag, 'server': widget.server}),
onAnyErr: () { onAnyErr: () {
// user should not be here // user should not be here
// close screen // close screen
@ -44,7 +44,7 @@ class _EditRoomPermissionSetPageState extends State<EditRoomPermissionSetPage> {
onOK: (body) async { onOK: (body) async {
final info = RoomInfo.fromJSON(body['data']); final info = RoomInfo.fromJSON(body['data']);
setState(() { setState(() {
permissions = info.permissions; permissions = info.permissions;
}); });
return true; return true;
}, },
@ -62,15 +62,7 @@ class _EditRoomPermissionSetPageState extends State<EditRoomPermissionSetPage> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Default permissions'), title: Text(AppLocalizations.of(context)!.roomDefaultPermissions),
leading: IconButton(
onPressed: () {
// go back
Navigator.of(context).pop();
},
icon: const Icon(Icons.arrow_back),
tooltip: "Go back",
),
), ),
body: ListView.builder( body: ListView.builder(
itemCount: items.length, itemCount: items.length,
@ -80,20 +72,20 @@ class _EditRoomPermissionSetPageState extends State<EditRoomPermissionSetPage> {
final int col = pow(2, index + 1) as int; final int col = pow(2, index + 1) as int;
return SwitchListTile( return SwitchListTile(
title: Text(RoomPermission.name(item)), title: Text(RoomPermission.name(item, context)),
subtitle: Text(RoomPermission.describe(item)), subtitle: Text(RoomPermission.describe(item, context)),
onChanged: (state) { onChanged: (state) {
setState(() { setState(() {
permissions += (state ? 1 : -1) * col; permissions += (state ? 1 : -1) * col;
}); });
}, },
value: permissions & col != 0); value: permissions & col != 0);
}, },
), ),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit),
tooltip: "Update default permission set", tooltip: AppLocalizations.of(context)!.updateRoomPermissionsHint,
label: const Text('Edit'), label: Text(AppLocalizations.of(context)!.updateRoomPermissions),
onPressed: () { onPressed: () {
final router = GoRouter.of(context); final router = GoRouter.of(context);
final sm = ScaffoldMessenger.of(context); final sm = ScaffoldMessenger.of(context);
@ -101,18 +93,18 @@ class _EditRoomPermissionSetPageState extends State<EditRoomPermissionSetPage> {
// update permissions // update permissions
doNetworkRequest(sm, doNetworkRequest(sm,
req: () => postWithCreadentials( req: () => postWithCreadentials(
path: 'setRoomRight', path: 'setRoomRight',
credentials: user, credentials: user,
target: user.server, target: user.server,
body: { body: {
'room': widget.tag, 'room': widget.tag,
'server': widget.server, 'server': widget.server,
'rights': permissions 'rights': permissions
}), }),
onOK: (_) { onOK: (_) {
router.pop(); router.pop();
}); });
}, },
), ),
); );

View file

@ -4,6 +4,7 @@ import 'package:outbag_app/backend/request.dart';
import 'package:outbag_app/backend/user.dart'; import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:outbag_app/tools/snackbar.dart'; import 'package:outbag_app/tools/snackbar.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class ChangePasswordDialog extends StatefulWidget { class ChangePasswordDialog extends StatefulWidget {
@ -21,59 +22,57 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AlertDialog( return AlertDialog(
title: const Text('Change Password'), title: Text(AppLocalizations.of(context)!.changeThemeTitle),
icon: const Icon(Icons.password), icon: const Icon(Icons.password),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column( child: Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: TextField( child: TextField(
controller: _ctrOldPassword, controller: _ctrOldPassword,
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
obscureText: true, obscureText: true,
decoration: const InputDecoration( decoration: InputDecoration(
prefixIcon: Icon(Icons.lock), prefixIcon: const Icon(Icons.lock),
labelText: 'Old Password', labelText: AppLocalizations.of(context)!.inputOldPasswordLabel,
hintText: 'Your current password', hintText: AppLocalizations.of(context)!.inputOldPasswordHint,
helperText: helperText:AppLocalizations.of(context)!.inputOldPasswordHelp,
'For safety, you have to type your current passwort', border: const OutlineInputBorder(),
border: OutlineInputBorder(), ),
), ),
), ),
), Padding(
Padding( padding: const EdgeInsets.all(8),
padding: const EdgeInsets.all(8), child: TextField(
child: TextField( controller: _ctrNewPassword,
controller: _ctrNewPassword, keyboardType: TextInputType.visiblePassword,
keyboardType: TextInputType.visiblePassword, obscureText: true,
obscureText: true, decoration: InputDecoration(
decoration: const InputDecoration( prefixIcon: const Icon(Icons.lock),
prefixIcon: Icon(Icons.lock), labelText: AppLocalizations.of(context)!.inputNewPasswordLabel,
labelText: 'New Password', hintText: AppLocalizations.of(context)!.inputNewPasswordHint,
hintText: 'Your new password', helperText:AppLocalizations.of(context)!.inputNewPasswordHelp,
helperText: 'Password have to be at least six characters long', border: const OutlineInputBorder(),
border: OutlineInputBorder(), ),
), ),
), ),
), Padding(
Padding( padding: const EdgeInsets.all(8),
padding: const EdgeInsets.all(8), child: TextField(
child: TextField( controller: _ctrNewPasswordRepeat,
controller: _ctrNewPasswordRepeat, keyboardType: TextInputType.visiblePassword,
keyboardType: TextInputType.visiblePassword, obscureText: true,
obscureText: true, decoration: InputDecoration(
decoration: const InputDecoration( prefixIcon: const Icon(Icons.lock),
prefixIcon: Icon(Icons.lock), labelText: AppLocalizations.of(context)!.inputNewPasswordRepeatLabel,
labelText: 'Repeat new Password', hintText: AppLocalizations.of(context)!.inputNewPasswordRepeatHint,
hintText: 'Type your new password again', helperText:AppLocalizations.of(context)!.inputNewPasswordRepeatHelp,
helperText: border: const OutlineInputBorder(),
'Type your new password again, to make sure you know it', ),
border: OutlineInputBorder(),
), ),
), ),
), ],
],
)), )),
actions: [ actions: [
TextButton( TextButton(
@ -81,20 +80,21 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
// close popup // close popup
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
child: const Text('Cancel'), child: Text(AppLocalizations.of(context)!.cancel),
), ),
FilledButton( FilledButton(
onPressed: () async { onPressed: () async {
final scaffMgr = ScaffoldMessenger.of(context); final scaffMgr = ScaffoldMessenger.of(context);
final nav = Navigator.of(context); final nav = Navigator.of(context);
final user = context.read<User>(); final user = context.read<User>();
final trans = AppLocalizations.of(context);
// validate password // validate password
if (_ctrNewPassword.text.length < 6) { if (_ctrNewPassword.text.length < 6) {
// password has to be at least 6 characters long // password has to be at least 6 characters long
showSimpleSnackbar(scaffMgr, showSimpleSnackbar(scaffMgr,
text: 'Password has to be at least 6 characters longs', text: trans!.errorPasswordLength,
action: 'Dismiss'); action: trans.dismiss);
_ctrNewPasswordRepeat.clear(); _ctrNewPasswordRepeat.clear();
return; return;
@ -102,7 +102,7 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
if (_ctrNewPassword.text != _ctrNewPasswordRepeat.text) { if (_ctrNewPassword.text != _ctrNewPasswordRepeat.text) {
// new passwords do not match // new passwords do not match
showSimpleSnackbar(scaffMgr, showSimpleSnackbar(scaffMgr,
text: 'New passwords do not match', action: 'Dismiss'); text: trans!.errorPasswordsDoNotMatch, action: trans.dismiss);
_ctrNewPasswordRepeat.clear(); _ctrNewPasswordRepeat.clear();
return; return;
@ -110,7 +110,7 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
if (hashPassword(_ctrOldPassword.text) != user.password) { if (hashPassword(_ctrOldPassword.text) != user.password) {
// current password wrong // current password wrong
showSimpleSnackbar(scaffMgr, showSimpleSnackbar(scaffMgr,
text: 'Old password is wrong', action: 'Dismiss'); text: trans!.errorOldPasswordWrong, action: trans.dismiss);
_ctrOldPassword.clear(); _ctrOldPassword.clear();
return; return;
@ -120,25 +120,25 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
// send request // send request
doNetworkRequest(scaffMgr, doNetworkRequest(scaffMgr,
req: () => postWithCreadentials( req: () => postWithCreadentials(
path: 'changePassword', path: 'changePassword',
target: user.server, target: user.server,
body: {'accountKey': password}, body: {'accountKey': password},
credentials: user), credentials: user),
onOK: (_) async { onOK: (_) async {
// update local user struct // update local user struct
final updatedUser = User( final updatedUser = User(
username: user.username, username: user.username,
password: password, password: password,
server: user.server); server: user.server);
await updatedUser.toDisk(); await updatedUser.toDisk();
}, },
after: () { after: () {
// close popup // close popup
nav.pop(); nav.pop();
}); });
}, },
child: const Text('Change password'), child: Text(AppLocalizations.of(context)!.changeThemeTitle),
) )
], ],
); );

View file

@ -6,6 +6,7 @@ import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/screens/settings/dialogs/password.dart'; import 'package:outbag_app/screens/settings/dialogs/password.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart'; import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class SettingsPage extends StatefulWidget { class SettingsPage extends StatefulWidget {
const SettingsPage({super.key}); const SettingsPage({super.key});
@ -26,7 +27,7 @@ class _SettingsPageState extends State<SettingsPage> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('Settings'), title: Text(AppLocalizations.of(context)!.settings),
), ),
body: SingleChildScrollView( body: SingleChildScrollView(
child: Center( child: Center(
@ -44,51 +45,58 @@ class _SettingsPageState extends State<SettingsPage> {
child: Text(user.humanReadable, child: Text(user.humanReadable,
style: textTheme.titleLarge)), style: textTheme.titleLarge)),
ListTile( ListTile(
title: const Text('Room count limit:'), title: Text(
subtitle: const Text( AppLocalizations.of(context)!.limitRoomCount),
'How many rooms you are allowed to own'), subtitle: Text(AppLocalizations.of(context)!
.limitRoomCountSubtitle),
trailing: Text('${meta?.maxRoomCount ?? ""}'), trailing: Text('${meta?.maxRoomCount ?? ""}'),
), ),
ListTile( ListTile(
title: const Text('Room size limit:'), title: Text(
subtitle: const Text( AppLocalizations.of(context)!.limitRoomSize),
'How many items/products/categories each room may contain'), subtitle: Text(AppLocalizations.of(context)!
.limitRoomSizeSubtitle),
trailing: Text('${meta?.maxRoomSize ?? ""}'), trailing: Text('${meta?.maxRoomSize ?? ""}'),
), ),
ListTile( ListTile(
title: const Text('Room member limit:'), title: Text(AppLocalizations.of(context)!
subtitle: const Text( .limitRoomMemberCount),
'How many members each of your rooms may have'), subtitle: Text(AppLocalizations.of(context)!
.limitRoomMemberCountSubtitle),
trailing: trailing:
Text('${meta?.maxRoomMemberCount ?? ""}')), Text('${meta?.maxRoomMemberCount ?? ""}')),
ListTile( ListTile(
title: const Text('Discoverable'), title: Text(AppLocalizations.of(context)!
subtitle: const Text( .userDiscoverable),
'Determines if your account can be discovered by users from other servers'), subtitle: Text(AppLocalizations.of(context)!
.userDiscoverableSubtitle),
trailing: Checkbox( trailing: Checkbox(
tristate: true, tristate: true,
value: meta?.discoverable, value: meta?.discoverable,
onChanged: (_) {}, onChanged: (_) {
// TODO: implement changeVisibility
},
)) ))
], ],
)))), )))),
// change theme button // change theme button
ListTile( ListTile(
title: const Text('Change Theme'), title: Text(AppLocalizations.of(context)!.changeThemeTitle),
subtitle: const Text( subtitle: Text(AppLocalizations.of(context)!.changeThemeSubtitle),
'You can change between a light theme, a dark theme and automatic theme selection'),
trailing: const Icon(Icons.chevron_right), trailing: const Icon(Icons.chevron_right),
onTap: () { onTap: () {
showDialog( showDialog(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Change Theme'), title: Text(
AppLocalizations.of(context)!.changeThemeTitle),
content: SingleChildScrollView( content: SingleChildScrollView(
child: Column(children: [ child: Column(children: [
const Padding( Padding(
padding: EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: Text('Choose your preferred theme'), child: Text(AppLocalizations.of(context)!
.changeThemeSubtitle),
), ),
SegmentedButton<AppTheme>( SegmentedButton<AppTheme>(
selected: {context.watch<AppTheme>()}, selected: {context.watch<AppTheme>()},
@ -100,7 +108,7 @@ class _SettingsPageState extends State<SettingsPage> {
return ButtonSegment<AppTheme>( return ButtonSegment<AppTheme>(
value: item, value: item,
icon: Icon(item.icon), icon: Icon(item.icon),
label: Text(item.name)); label: Text(item.name(context)));
}).toList(), }).toList(),
onSelectionChanged: (item) async { onSelectionChanged: (item) async {
try { try {
@ -111,9 +119,9 @@ class _SettingsPageState extends State<SettingsPage> {
])), ])),
actions: [ actions: [
FilledButton( FilledButton(
child: const Text('Close'), child: Text(AppLocalizations.of(context)!.close),
onPressed: () { onPressed: () {
Navigator.of(context).pop(); context.pop();
}, },
) )
], ],
@ -123,8 +131,9 @@ class _SettingsPageState extends State<SettingsPage> {
// change password button // change password button
ListTile( ListTile(
title: const Text('Change password'), title: Text(AppLocalizations.of(context)!.changePasswordTitle),
subtitle: const Text('Choose a new password for your account'), subtitle:
Text(AppLocalizations.of(context)!.changePasswordSubtitle),
onTap: () { onTap: () {
showDialog( showDialog(
context: context, context: context,
@ -135,8 +144,8 @@ class _SettingsPageState extends State<SettingsPage> {
// export account to json // export account to json
ListTile( ListTile(
title: const Text('Export account'), title: Text(AppLocalizations.of(context)!.exportAccountTitle),
subtitle: const Text('Export account data'), subtitle: Text(AppLocalizations.of(context)!.exportAccountSubtitle),
onTap: () { onTap: () {
// TODO: show confirm dialog // TODO: show confirm dialog
// NOTE: json dump the localstore // NOTE: json dump the localstore
@ -148,8 +157,8 @@ class _SettingsPageState extends State<SettingsPage> {
// delete account button // delete account button
ListTile( ListTile(
title: const Text('Delete account'), title: Text(AppLocalizations.of(context)!.deleteAccountTitle),
subtitle: const Text('Delete your account from your homeserver'), subtitle: Text(AppLocalizations.of(context)!.deleteAccountSubtitle),
onTap: () { onTap: () {
// show confirm dialog // show confirm dialog
// NOTE: same as logout // NOTE: same as logout
@ -158,16 +167,17 @@ class _SettingsPageState extends State<SettingsPage> {
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: const Text('Delete account'), title: Text(
content: const Text( AppLocalizations.of(context)!.deleteAccountTitle),
'Do you really want to delete your account?'), content: Text(
AppLocalizations.of(context)!.deleteAccountConfirm),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
// close popup // close popup
Navigator.of(ctx).pop(); Navigator.of(ctx).pop();
}, },
child: const Text('Cancel'), child: Text(AppLocalizations.of(context)!.cancel),
), ),
FilledButton( FilledButton(
onPressed: () async { onPressed: () async {
@ -198,7 +208,7 @@ class _SettingsPageState extends State<SettingsPage> {
nav.pop(); nav.pop();
}); });
}, },
child: const Text('Delete Account'), child: Text(AppLocalizations.of(context)!.yes),
) )
], ],
)); ));
@ -210,22 +220,23 @@ class _SettingsPageState extends State<SettingsPage> {
Padding( Padding(
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(8),
child: FilledButton.tonal( child: FilledButton.tonal(
child: const Text('Log out'), child: Text(AppLocalizations.of(context)!.logOut),
onPressed: () { onPressed: () {
// show confirm dialog // show confirm dialog
showDialog( showDialog(
context: context, context: context,
builder: (ctx) => AlertDialog( builder: (ctx) => AlertDialog(
title: const Text('Log out'), title: Text(AppLocalizations.of(context)!.logOut),
content: content: Text(
const Text('Do you really want to log out?'), AppLocalizations.of(context)!.logOutConfirm),
actions: [ actions: [
TextButton( TextButton(
onPressed: () { onPressed: () {
// close popup // close popup
Navigator.of(ctx).pop(); Navigator.of(ctx).pop();
}, },
child: const Text('Cancel'), child:
Text(AppLocalizations.of(context)!.cancel),
), ),
FilledButton( FilledButton(
onPressed: () async { onPressed: () async {
@ -242,7 +253,7 @@ class _SettingsPageState extends State<SettingsPage> {
// go back home // go back home
router.pushReplacementNamed('home'); router.pushReplacementNamed('home');
}, },
child: const Text('Log out'), child: Text(AppLocalizations.of(context)!.yes),
) )
], ],
)); ));

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:outbag_app/tools/assets.dart'; import 'package:outbag_app/tools/assets.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'dart:math'; import 'dart:math';
class WelcomePage extends StatefulWidget { class WelcomePage extends StatefulWidget {
@ -33,17 +34,17 @@ class _WelcomePageState extends State<WelcomePage> {
.textTheme .textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface); .apply(displayColor: Theme.of(context).colorScheme.onSurface);
String fabText = "Next"; String fabText = AppLocalizations.of(context)!.next;
if (_currentPage == 0) { if (_currentPage == 0) {
fabText = "Let's go"; fabText = AppLocalizations.of(context)!.letsGo;
} else if (_currentPage == 4 - 1) { } else if (_currentPage == 4 - 1) {
fabText = "Sign up"; fabText = AppLocalizations.of(context)!.signUp;
} }
String fabTooltip = "Continue Tour"; String fabTooltip = AppLocalizations.of(context)!.continueTour;
if (_currentPage == 0) { if (_currentPage == 0) {
fabTooltip = "Take Tour"; fabTooltip = AppLocalizations.of(context)!.takeTour;
} else if (_currentPage == 4 - 1) { } else if (_currentPage == 4 - 1) {
fabTooltip = "Create an account"; fabTooltip = AppLocalizations.of(context)!.createNewAccount;
} }
double width = MediaQuery.of(context).size.width; double width = MediaQuery.of(context).size.width;
@ -66,10 +67,13 @@ class _WelcomePageState extends State<WelcomePage> {
width: smallest * 0.5, width: smallest * 0.5,
height: smallest * 0.5), height: smallest * 0.5),
Text( Text(
'Welcome to Outbag', AppLocalizations.of(context)!.welcomeTitle,
style: textTheme.displaySmall, style: textTheme.displaySmall,
), ),
Text('Shopping lists made easy', style: textTheme.bodyMedium) Text(
AppLocalizations.of(context)!.welcomeSubtitle,
style: textTheme.bodyMedium
)
], ],
), ),
Column( Column(
@ -81,10 +85,11 @@ class _WelcomePageState extends State<WelcomePage> {
width: smallest * 0.5, width: smallest * 0.5,
height: smallest * 0.5), height: smallest * 0.5),
Text( Text(
'Open. Decentralized', AppLocalizations.of(context)!.page2Title,
style: textTheme.displaySmall, style: textTheme.displaySmall,
), ),
Text('One account, multiple servers', Text(
AppLocalizations.of(context)!.page2Subtitle,
style: textTheme.bodyMedium) style: textTheme.bodyMedium)
], ],
), ),
@ -97,10 +102,11 @@ class _WelcomePageState extends State<WelcomePage> {
width: smallest * 0.5, width: smallest * 0.5,
height: smallest * 0.5), height: smallest * 0.5),
Text( Text(
'Made to share', AppLocalizations.of(context)!.page3Title,
style: textTheme.displaySmall, style: textTheme.displaySmall,
), ),
Text('Collaborate on your shopping list in real time', Text(
AppLocalizations.of(context)!.page3Subtitle,
style: textTheme.bodyMedium) style: textTheme.bodyMedium)
], ],
), ),
@ -113,10 +119,11 @@ class _WelcomePageState extends State<WelcomePage> {
width: smallest * 0.5, width: smallest * 0.5,
height: smallest * 0.5), height: smallest * 0.5),
Text( Text(
'Pocket-sized', AppLocalizations.of(context)!.page4Title,
style: textTheme.displaySmall, style: textTheme.displaySmall,
), ),
Text('Always have your shopping list with you', Text(
AppLocalizations.of(context)!.page4Subtitle,
style: textTheme.bodyMedium) style: textTheme.bodyMedium)
], ],
), ),
@ -126,7 +133,8 @@ class _WelcomePageState extends State<WelcomePage> {
onPressed: () { onPressed: () {
context.goNamed('signin'); context.goNamed('signin');
}, },
child: const Text('I already have an account'), child: Text(AppLocalizations.of(context)!.userHasAnAccount
),
) )
], ],
), ),

View file

@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:outbag_app/backend/errors.dart'; import 'package:outbag_app/backend/errors.dart';
import 'package:outbag_app/backend/request.dart'; import 'package:outbag_app/backend/request.dart';
import 'package:outbag_app/tools/snackbar.dart'; import 'package:outbag_app/tools/snackbar.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void doNetworkRequest(ScaffoldMessengerState? sm, void doNetworkRequest(ScaffoldMessengerState? sm,
{required Future<Response> Function() req, {required Future<Response> Function() req,
@ -11,6 +12,8 @@ void doNetworkRequest(ScaffoldMessengerState? sm,
Function()? onAnyErr, Function()? onAnyErr,
Function()? after, Function()? after,
bool Function(Map<String, dynamic>)? onServerErr}) async { bool Function(Map<String, dynamic>)? onServerErr}) async {
AppLocalizations? trans = (sm!=null)?AppLocalizations.of(sm.context):null;
Response res; Response res;
try { try {
res = await req(); res = await req();
@ -23,7 +26,7 @@ void doNetworkRequest(ScaffoldMessengerState? sm,
} }
if (showBar && sm != null) { if (showBar && sm != null) {
showSimpleSnackbar(sm, text: 'Network Error', action: 'Dismiss'); showSimpleSnackbar(sm, text: trans!.errorNetwork, action: trans.dismiss);
} }
if (onAnyErr != null) { if (onAnyErr != null) {
onAnyErr(); onAnyErr();
@ -47,7 +50,7 @@ void doNetworkRequest(ScaffoldMessengerState? sm,
showBar = onServerErr(res.body); showBar = onServerErr(res.body);
} }
if (showBar && sm != null) { if (showBar && sm != null) {
showSimpleSnackbar(sm, text: errorAsString(res.body), action: 'OK'); showSimpleSnackbar(sm, text: errorAsString(res.body, trans!), action: trans.ok);
} }
if (onAnyErr != null) { if (onAnyErr != null) {
onAnyErr(); onAnyErr();
@ -61,7 +64,7 @@ void doNetworkRequest(ScaffoldMessengerState? sm,
} }
if (showBar && sm != null) { if (showBar && sm != null) {
showSimpleSnackbar(sm, text: 'Unknown Error', action: 'OK'); showSimpleSnackbar(sm, text: trans!.errorUnknown, action: trans.ok);
} }
} }

View file

@ -134,6 +134,11 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_svg: flutter_svg:
dependency: "direct main" dependency: "direct main"
description: description:
@ -184,6 +189,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "4.0.15" version: "4.0.15"
intl:
dependency: "direct main"
description:
name: intl
sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91"
url: "https://pub.dev"
source: hosted
version: "0.17.0"
js: js:
dependency: transitive dependency: transitive
description: description:

View file

@ -36,6 +36,9 @@ dependencies:
crypto: ^3.0.2 crypto: ^3.0.2
provider: ^6.0.5 provider: ^6.0.5
go_router: ^6.5.0 go_router: ^6.5.0
flutter_localizations:
sdk: flutter
intl: any
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -54,7 +57,8 @@ dev_dependencies:
# The following section is specific to Flutter packages. # The following section is specific to Flutter packages.
flutter: flutter:
# needed for l10n localizations
generate: true
# The following line ensures that the Material Icons font is # The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in # included with your application, so that you can use the icons in
# the material Icons class. # the material Icons class.