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
*/
String errorAsString(Map<String, dynamic> json) {
String errorAsString(Map<String, dynamic> json, AppLocalizations trans) {
switch (json['data']) {
case 'notfound':
return 'Endpoint not found';
return trans.errorNotFound;
case 'wrongstate':
return 'Missing data';
return trans.errorDataIncomplete;
case 'data':
return 'Invalid data';
return trans.errorDataInvalid;
case 'roomAdmin':
case 'right':
return 'You are not allowed to perform this action';
return trans.errorPermissions;
case 'server':
return 'Server error';
return trans.errorServer;
case 'closed':
return 'Server cannot be reached';
return trans.errorUnreachable;
case 'auth':
return 'Username or password wrong';
return trans.errorAuth;
case 'ota':
return 'Invalid OTA';
return trans.errorInvalidOTA;
case 'existence':
return 'Username unavailable';
return trans.errorUsernameUnavailable;
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
// according to the 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) {
case 1:
return 'Add Articles';
return trans!.roomPermissionAddArticles;
case 2:
return 'Remove Articles';
return trans!.roomPermissionRemoveArticles;
case 4:
return 'List Groups and Items';
return trans!.roomPermissionList;
case 8:
return 'Change Room Metadata';
return trans!.roomPermissionChangeMeta;
case 16:
return 'Manage OTAs';
return trans!.roomPermissionManageOTA;
case 32:
return 'Manage Admins';
return trans!.roomPermissionManageAdmins;
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) {
case 1:
return 'Allows users to add items to the shopping list';
return trans!.roomPermissionAddArticlesSubtitle;
case 2:
return 'Allows users to remove items from the shopping list';
return trans!.roomPermissionRemoveArticlesSubtitle;
case 4:
return 'Allows the user to view groups and products';
return trans!.roomPermissionListSubtitle;
case 8:
return 'Allows the user to edit the room name, description and icon';
return trans!.roomPermissionChangeMetaSubtitle;
case 16:
return 'Alloww the user to create, share and delete authentification tokens';
return trans!.roomPermissionManageOTASubtitle;
case 32:
return 'Allows the user to change the admin status of other members';
return trans!.roomPermissionManageAdminsSubtitle;
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:localstore/localstore.dart';
import 'package:outbag_app/tools/assets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class RoomVisibility {
final int type;
@ -28,14 +29,16 @@ class RoomVisibility {
return Icons.lock;
}
String get text {
String text(BuildContext context) {
final trans = AppLocalizations.of(context);
if (type == 2) {
return "Global";
return trans!.roomVisibilityGlobal;
} else if (type == 1) {
return "Local";
return trans!.roomVisibilityLocal;
}
return "Private";
return trans!.roomVisibilityPrivate;
}
static List<RoomVisibility> list() {

View file

@ -1,20 +1,23 @@
import 'package:flutter/material.dart';
import 'package:localstore/localstore.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class AppTheme {
ThemeMode mode;
AppTheme(this.mode);
String get name {
String name(BuildContext context) {
final trans = AppLocalizations.of(context);
if (mode == ThemeMode.light) {
return 'Light';
return trans!.themeLight;
}
if (mode == ThemeMode.dark) {
return 'Dark';
return trans!.themeDark;
}
return 'System';
return trans!.themeSystem;
}
IconData get icon {
@ -80,5 +83,4 @@ class AppTheme {
@override
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/tools/fetch_wrapper.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/welcome.dart';
import './screens/room/main.dart';
@ -142,6 +144,13 @@ class _OutbagAppState extends State {
],
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),
@ -223,10 +232,6 @@ class _OutbagAppState extends State {
GoRoute(
name: 'room',
path: 'r/:server/:id',
redirect: (context, state) {
print(state.subloc);
return null;
},
builder: (context, state) => RoomPage(
state.params['server'] ?? '',
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/tools/fetch_wrapper.dart';
import 'package:outbag_app/tools/snackbar.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../backend/resolve_url.dart';
enum Mode {
@ -31,13 +32,13 @@ class _AuthPageState extends State<AuthPage> {
@override
Widget build(BuildContext context) {
String modeName = "Sign In";
String modeName = AppLocalizations.of(context)!.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) {
modeDescription = "Create new account";
modeDescription = AppLocalizations.of(context)!.createNewAccount;
}
final textTheme = Theme.of(context)
@ -52,7 +53,7 @@ class _AuthPageState extends State<AuthPage> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
Text('Loading', style: textTheme.titleLarge),
Text(AppLocalizations.of(context)!.loading, style: textTheme.titleLarge),
])))
: Scaffold(
appBar: AppBar(
@ -70,13 +71,12 @@ class _AuthPageState extends State<AuthPage> {
child: TextField(
controller: _ctrServer,
keyboardType: TextInputType.url,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.dns),
labelText: 'Server',
hintText: 'Your homeserver url',
helperText:
'Your data will be stored on your homeserver',
border: OutlineInputBorder(),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.dns),
labelText: AppLocalizations.of(context)!.inputServerLabel,
hintText: AppLocalizations.of(context)!.inputServerHint,
helperText:AppLocalizations.of(context)!.inputServerHelp,
border: const OutlineInputBorder(),
),
),
),
@ -85,13 +85,12 @@ class _AuthPageState extends State<AuthPage> {
child: TextField(
controller: _ctrUsername,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.person),
labelText: 'Username',
hintText: 'Your username',
helperText:
'your username and server tag allow others to identify you',
border: OutlineInputBorder(),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.person),
labelText: AppLocalizations.of(context)!.inputUsernameLabel,
hintText: AppLocalizations.of(context)!.inputUsernameHint,
helperText:AppLocalizations.of(context)!.inputUsernameHelp,
border: const OutlineInputBorder(),
),
),
),
@ -101,13 +100,12 @@ class _AuthPageState extends State<AuthPage> {
controller: _ctrPassword,
keyboardType: TextInputType.visiblePassword,
obscureText: true,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.lock),
labelText: 'Password',
hintText: 'Your password',
helperText:
'Password have to be at least six characters long',
border: OutlineInputBorder(),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock),
labelText: AppLocalizations.of(context)!.inputPasswordLabel,
hintText: AppLocalizations.of(context)!.inputPasswordHint,
helperText:AppLocalizations.of(context)!.inputPasswordHelp,
border: const OutlineInputBorder(),
),
),
),
@ -120,13 +118,12 @@ class _AuthPageState extends State<AuthPage> {
controller: _ctrPasswordRpt,
keyboardType: TextInputType.visiblePassword,
obscureText: true,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.lock),
labelText: 'Repeat Password',
hintText: 'Type your password again',
helperText:
'Make sure to type the correct password',
border: OutlineInputBorder(),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.lock),
labelText: AppLocalizations.of(context)!.inputPasswordRepeatLabel,
hintText: AppLocalizations.of(context)!.inputPasswordRepeatHint,
helperText:AppLocalizations.of(context)!.inputPasswordRepeatHelp,
border: const OutlineInputBorder(),
),
),
)
@ -140,13 +137,12 @@ class _AuthPageState extends State<AuthPage> {
child: TextField(
controller: _ctrOTA,
keyboardType: TextInputType.visiblePassword,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.key),
labelText: 'OTA',
hintText: 'One-Time-Authorization token',
helperText:
'This token might be required if the server is rate limited',
border: OutlineInputBorder(),
decoration: InputDecoration(
prefixIcon: const Icon(Icons.key),
labelText: AppLocalizations.of(context)!.inputOTALabel,
hintText: AppLocalizations.of(context)!.inputOTAHint,
helperText:AppLocalizations.of(context)!.inputOTAHelp,
border: const OutlineInputBorder(),
),
),
)
@ -170,7 +166,8 @@ class _AuthPageState extends State<AuthPage> {
});
showSimpleSnackbar(scaffMgr,
text: 'Passwords do not match', action: 'Dismiss');
text: AppLocalizations.of(context)!.errorPasswordsDoNotMatch,
action: AppLocalizations.of(context)!.dismiss);
_ctrPasswordRpt.clear();
return;
@ -184,8 +181,8 @@ class _AuthPageState extends State<AuthPage> {
});
showSimpleSnackbar(scaffMgr,
text: 'Password has to be at least 6 characters longs',
action: 'Dismiss');
text: AppLocalizations.of(context)!.errorPasswordLength,
action: AppLocalizations.of(context)!.dismiss);
_ctrPasswordRpt.clear();
return;
@ -204,9 +201,8 @@ class _AuthPageState extends State<AuthPage> {
});
showSimpleSnackbar(scaffMgr,
text:
'Unable to find valid outbag server on ${_ctrServer.text}',
action: 'Dismiss');
text: AppLocalizations.of(context)!.errorInvalidServer(_ctrServer.text),
action: AppLocalizations.of(context)!.dismiss);
return;
}

View file

@ -6,6 +6,7 @@ import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart';
import 'package:go_router/go_router.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import '../backend/room.dart';
class HomePage extends StatefulWidget {
@ -69,7 +70,7 @@ class _HomePageState extends State<HomePage> {
actions: [
IconButton(
icon: const Icon(Icons.search),
tooltip: "Search",
tooltip: AppLocalizations.of(context)!.search,
onPressed: () {
// show searchbar
// NOTE: location currently unknown
@ -91,7 +92,7 @@ class _HomePageState extends State<HomePage> {
menuChildren: [
MenuItemButton(
leadingIcon: const Icon(Icons.settings),
child: const Text('Settings'),
child: Text(AppLocalizations.of(context)!.settings),
onPressed: () {
// show settings screen
context.goNamed('settings');
@ -103,7 +104,7 @@ class _HomePageState extends State<HomePage> {
? [
MenuItemButton(
leadingIcon: const Icon(Icons.dns),
child: const Text('Server Dashboard'),
child: Text(AppLocalizations.of(context)!.serverDashboard),
onPressed: () {
// show settings screen
context.goNamed('dash');
@ -112,7 +113,7 @@ class _HomePageState extends State<HomePage> {
: [],
MenuItemButton(
leadingIcon: const Icon(Icons.info_rounded),
child: const Text('About'),
child: Text(AppLocalizations.of(context)!.about),
onPressed: () {
// show about screen
context.goNamed('about');
@ -154,13 +155,13 @@ class _HomePageState extends State<HomePage> {
},
),
floatingActionButton: FloatingActionButton.extended(
label: const Text('Add Room'),
label: Text(AppLocalizations.of(context)!.addRoom),
icon: const Icon(Icons.add),
onPressed: () {
// create new 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_svg/flutter_svg.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/room.dart';
import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:outbag_app/tools/snackbar.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'dart:math';
class EditRoomPage extends StatefulWidget {
@ -69,7 +70,7 @@ class _EditRoomPageState extends State<EditRoomPage> {
} catch (_) {
// no room data available
// close screen
router.pushReplacementNamed('homoe');
router.pushReplacementNamed('home');
}
})();
return true;
@ -77,7 +78,7 @@ class _EditRoomPageState extends State<EditRoomPage> {
onServerErr: (json) {
// user not allowed to be here
// close screen
router.pushReplacementNamed('homoe');
router.pushReplacementNamed('home');
return true;
},
);
@ -127,11 +128,11 @@ class _EditRoomPageState extends State<EditRoomPage> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
Text('Loading', style: textTheme.titleLarge),
Text(AppLocalizations.of(context)!.loading, style: textTheme.titleLarge),
])))
: Scaffold(
appBar: AppBar(
title: const Text('Edit Room'),
title: Text(AppLocalizations.of(context)!.editRoomMetadataShort),
),
body: SingleChildScrollView(
child: Center(
@ -149,13 +150,13 @@ class _EditRoomPageState extends State<EditRoomPage> {
width: smallest * 0.3,
height: smallest * 0.3,
),
tooltip: 'Change room icon',
tooltip: AppLocalizations.of(context)!.changeRoomIcon,
onPressed: () {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text(
'Choose a room Icon'),
title: Text(
AppLocalizations.of(context)!.chooseRoomIcon),
actions: const [],
content: SizedBox(
width: smallest * 0.3 * 3,
@ -195,13 +196,15 @@ class _EditRoomPageState extends State<EditRoomPage> {
enabled: false,
controller: _ctrID,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.fact_check),
labelText: 'Room ID',
hintText: 'Unique room id',
helperText:
'the room id and server tag allow the room to be identified',
border: OutlineInputBorder(),
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(),
),
),
),
@ -210,13 +213,15 @@ class _EditRoomPageState extends State<EditRoomPage> {
child: TextField(
controller: _ctrName,
keyboardType: TextInputType.name,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.badge),
labelText: 'Room Name',
hintText: 'Give your room a name',
helperText:
'Easily identify a room with a human readable name',
border: OutlineInputBorder(),
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(),
),
),
),
@ -225,13 +230,15 @@ class _EditRoomPageState extends State<EditRoomPage> {
child: TextField(
controller: _ctrDescription,
keyboardType: TextInputType.text,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.dns),
labelText: 'Room Description',
hintText: 'Briefly describe your Room',
helperText:
'Make it easier for other to know what this room is used for',
border: OutlineInputBorder(),
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(),
),
),
),
@ -241,22 +248,12 @@ class _EditRoomPageState extends State<EditRoomPage> {
onPressed: () async {
final scaffMgr = ScaffoldMessenger.of(context);
final nav = Navigator.of(context);
final trans = AppLocalizations.of(context);
// name may not be empty
if (_ctrName.text.isEmpty) {
final snackBar = SnackBar(
behavior: SnackBarBehavior.floating,
content: const Text('Please specify a room name'),
action: SnackBarAction(
label: 'Ok',
onPressed: () {
scaffMgr.hideCurrentSnackBar();
},
),
);
scaffMgr.hideCurrentSnackBar();
scaffMgr.showSnackBar(snackBar);
showSimpleSnackbar(scaffMgr,
text: trans!.errorNoRoomName, action: trans.ok);
return;
}
@ -275,8 +272,8 @@ class _EditRoomPageState extends State<EditRoomPage> {
clone.description = _ctrDescription.text;
clone.icon = _ctrIcon;
try {
final resp = await postWithCreadentials(
doNetworkRequest(scaffMgr,
req: ()=>postWithCreadentials(
target: user.server,
credentials: user,
path: 'changeRoomMeta',
@ -286,45 +283,16 @@ class _EditRoomPageState extends State<EditRoomPage> {
'title': clone.name,
'description': clone.description,
'icon': clone.icon?.type,
});
if (resp.res == Result.ok) {
}),
onOK: (_) async {
// room was created
// save room
await clone.toDisk();
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)),
);
}

View file

@ -6,6 +6,7 @@ import 'package:outbag_app/backend/room.dart';
import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'dart:math';
class JoinRoomPage extends StatefulWidget {
@ -102,7 +103,7 @@ class _JoinRoomPageState extends State {
actions: [
IconButton(
icon: const Icon(Icons.search),
tooltip: "Search",
tooltip: AppLocalizations.of(context)!.search,
onPressed: () {
// show searchbar
// NOTE: location currently unknown
@ -110,7 +111,7 @@ class _JoinRoomPageState extends State {
),
IconButton(
icon: const Icon(Icons.refresh),
tooltip: "Refresh",
tooltip: AppLocalizations.of(context)!.refresh,
onPressed: () {
// fetch public rooms again
didChangeDependencies();
@ -132,7 +133,7 @@ class _JoinRoomPageState extends State {
menuChildren: [
MenuItemButton(
leadingIcon: const Icon(Icons.drafts),
child: const Text('Join invite-only room'),
child: Text(AppLocalizations.of(context)!.joinRoomInvite),
onPressed: () {
// show settings screen
context.goNamed('join-room-ota');
@ -146,7 +147,7 @@ class _JoinRoomPageState extends State {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text('No new Rooms found', style: textTheme.titleLarge),
Text(AppLocalizations.of(context)!.noNewRoomsFound, style: textTheme.titleLarge),
],
))
: ListView.builder(
@ -159,7 +160,7 @@ class _JoinRoomPageState extends State {
semanticContainer: true,
child: InkWell(
onTap: () {
// TODO: show modalBottomSheet
// show modalBottomSheet
// with room information
// and join button
showModalBottomSheet(
@ -202,7 +203,7 @@ class _JoinRoomPageState extends State {
children: [
Icon(room.visibility?.icon),
Text((room
.visibility?.text)!),
.visibility?.text(context))!),
]),
])),
// action buttons
@ -217,7 +218,7 @@ class _JoinRoomPageState extends State {
child: ElevatedButton.icon(
icon:
const Icon(Icons.close),
label: const Text('Cancel'),
label: Text(AppLocalizations.of(context)!.cancel),
onPressed: () {
// close sheet
Navigator.pop(context);
@ -230,7 +231,7 @@ class _JoinRoomPageState extends State {
child: FilledButton.icon(
icon:
const Icon(Icons.check),
label: const Text('Join'),
label: Text(AppLocalizations.of(context)!.joinRoom),
onPressed: () async {
final scaffMgr =
ScaffoldMessenger.of(
@ -294,13 +295,13 @@ class _JoinRoomPageState extends State {
},
),
floatingActionButton: FloatingActionButton.extended(
label: const Text('New'),
label: Text(AppLocalizations.of(context)!.newRoom),
icon: const Icon(Icons.add),
onPressed: () {
// create 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/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class RoomPage extends StatefulWidget {
final String server;
@ -121,23 +122,27 @@ class _RoomPageState extends State<RoomPage> {
duration: const Duration(milliseconds: 300));
},
selectedIndex: page,
destinations: const [
destinations: [
NavigationDestination(
icon: Icon(Icons.list),
label: "List",
tooltip: 'View shopping list'),
icon: const Icon(Icons.list),
label: AppLocalizations.of(context)!.roomListTitle,
tooltip: AppLocalizations.of(context)!.roomListSubtitle
),
NavigationDestination(
icon: Icon(Icons.inventory_2),
label: "Products",
tooltip: 'View saved items'),
icon: const Icon(Icons.inventory_2),
label: AppLocalizations.of(context)!.roomProductsTitle,
tooltip: AppLocalizations.of(context)!.roomProductsSubtitle
),
NavigationDestination(
icon: Icon(Icons.category),
label: "Categories",
tooltip: 'View categories'),
icon: const Icon(Icons.category),
label: AppLocalizations.of(context)!.roomCategoriesTitle,
tooltip: AppLocalizations.of(context)!.roomCategoriesSubtitle
),
NavigationDestination(
icon: Icon(Icons.info_rounded),
label: "About",
tooltip: 'View room info'),
icon: const Icon(Icons.info_rounded),
label: AppLocalizations.of(context)!.roomAboutTitle,
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/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class ManageRoomMembersPage extends StatefulWidget {
final String server;
@ -95,15 +96,8 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
return Scaffold(
appBar: AppBar(
title: Text('Room Members (${list.length})'),
leading: IconButton(
onPressed: () {
// go back
Navigator.of(context).pop();
},
icon: const Icon(Icons.arrow_back),
tooltip: "Go back",
),
title: Text(
AppLocalizations.of(context)!.roomMembersTitle(list.length)),
//actions: [
// // NOTE: Maybe add a search icon
// // and general search functionality here
@ -113,13 +107,13 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
itemBuilder: (BuildContext context, int index) {
final item = list[index];
String role = "Member";
String role = AppLocalizations.of(context)!.roleMember;
if (info != null &&
(info?.owner)! == item.id &&
widget.server == item.serverTag) {
role = "Owner";
role = AppLocalizations.of(context)!.roleOwner;
} else if (item.isAdmin) {
role = "Admin";
role = AppLocalizations.of(context)!.roleAdmin;
}
bool enable = true;
@ -172,11 +166,11 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
leading: const Icon(
Icons.supervisor_account),
title: Text(item.isAdmin
? 'Remove admin privileges'
: 'Make Admin'),
? AppLocalizations.of(context)!.removeAdminTitle
: AppLocalizations.of(context)!.makeAdminTitle),
subtitle: Text(item.isAdmin
? 'Revokes admin privileges from the user'
: 'Grants the user the permission to do everything'),
? AppLocalizations.of(context)!.removeAdminSubtitle
: AppLocalizations.of(context)!.makeAdminSubtitle),
onTap: () {
// make user admin
showDialog(
@ -189,12 +183,12 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
.supervisor_account),
title: Text(item
.isAdmin
? 'Remove admin privileges'
: 'Make Admin'),
? AppLocalizations.of(context)!.removeAdminTitle
: AppLocalizations.of(context)!.makeAdminTitle),
content: Text(item
.isAdmin
? "Do you really want to remove ${item.humanReadableName}'s admin privileges"
: 'Do you really want to make ${item.humanReadableName} admin?'),
? AppLocalizations.of(context)!.removeAdminConfirm(item.humanReadableName)
: AppLocalizations.of(context)!.makeAdminConfirm(item.humanReadableName)),
actions: [
TextButton(
onPressed:
@ -205,8 +199,7 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
Navigator.of(context)
.pop();
},
child: const Text(
'Cancel'),
child: Text(AppLocalizations.of(context)!.cancel),
),
FilledButton(
onPressed:
@ -238,8 +231,7 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
nav.pop();
});
},
child: const Text(
'OK'),
child: Text(AppLocalizations.of(context)!.ok),
)
],
));
@ -257,10 +249,8 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
ListTile(
leading: const Icon(
Icons.person_remove),
title:
const Text('Kick User'),
subtitle: const Text(
"Temporarrily remove user from server (they'll be able to join the room again)"),
title: Text(AppLocalizations.of(context)!.kickUserTitle),
subtitle: Text(AppLocalizations.of(context)!.kickUserSubtitle),
onTap: () {
// remove user from room
showDialog(
@ -271,10 +261,10 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
icon: const Icon(
Icons
.person_remove),
title: const Text(
'Kick User'),
title: Text(
AppLocalizations.of(context)!.kickUserTitle),
content: Text(
'Do you really want to kick ${item.humanReadableName}?'),
AppLocalizations.of(context)!.kichUserConfirm(item.humanReadableName)),
actions: [
TextButton(
onPressed:
@ -286,8 +276,8 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
Navigator.of(ctx)
.pop();
},
child: const Text(
'Cancel'),
child: Text(
AppLocalizations.of(context)!.cancel),
),
FilledButton(
onPressed:
@ -318,8 +308,8 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
nav.pop();
});
},
child: const Text(
'Kick User'),
child: Text(
AppLocalizations.of(context)!.ok),
)
],
));
@ -331,7 +321,7 @@ class _ManageRoomMembersPageState extends State<ManageRoomMembersPage> {
),
),
FilledButton(
child: const Text('Close'),
child: Text(AppLocalizations.of(context)!.close),
onPressed: () {
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/snackbar.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'dart:math';
class NewRoomPage extends StatefulWidget {
@ -43,11 +44,12 @@ class _NewRoomPageState extends State {
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
Text('Loading', style: textTheme.titleLarge),
Text(AppLocalizations.of(context)!.loading,
style: textTheme.titleLarge),
])))
: Scaffold(
appBar: AppBar(
title: const Text('New Room'),
title: Text(AppLocalizations.of(context)!.newRoom),
),
body: SingleChildScrollView(
child: Center(
@ -65,13 +67,15 @@ class _NewRoomPageState extends State {
width: smallest * 0.3,
height: smallest * 0.3,
),
tooltip: 'Change room icon',
tooltip: AppLocalizations.of(context)!
.changeRoomIcon,
onPressed: () {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text(
'Choose a room Icon'),
title: Text(
AppLocalizations.of(context)!
.chooseRoomIcon),
actions: const [],
content: SizedBox(
width: smallest * 0.3 * 3,
@ -92,8 +96,11 @@ class _NewRoomPageState extends State {
smallest *
0.3,
),
tooltip:
icon.text,
// 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(() {
_ctrIcon =
@ -112,13 +119,15 @@ class _NewRoomPageState extends State {
child: TextField(
controller: _ctrID,
keyboardType: TextInputType.emailAddress,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.fact_check),
labelText: 'Room ID',
hintText: 'Unique room id',
helperText:
'the room id and server tag allow the room to be identified',
border: OutlineInputBorder(),
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(),
),
),
),
@ -127,13 +136,15 @@ class _NewRoomPageState extends State {
child: TextField(
controller: _ctrName,
keyboardType: TextInputType.name,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.badge),
labelText: 'Room Name',
hintText: 'Give your room a name',
helperText:
'Easily identify a room with a human readable name',
border: OutlineInputBorder(),
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(),
),
),
),
@ -142,18 +153,25 @@ class _NewRoomPageState extends State {
child: TextField(
controller: _ctrDescription,
keyboardType: TextInputType.text,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.dns),
labelText: 'Room Description',
hintText: 'Briefly describe your Room',
helperText:
'Make it easier for other to know what this room is used for',
border: OutlineInputBorder(),
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(),
),
),
),
Text('Visibility', style: textTheme.labelLarge),
Text('Specify who has access to your room',
Text(
AppLocalizations.of(context)!
.roomVisibilityTitle,
style: textTheme.labelLarge),
Text(
AppLocalizations.of(context)!
.roomVisibilitySubtitle,
style: textTheme.bodySmall),
SegmentedButton<RoomVisibility>(
showSelectedIcon: true,
@ -162,7 +180,7 @@ class _NewRoomPageState extends State {
segments: RoomVisibility.list().map((vis) {
return ButtonSegment<RoomVisibility>(
value: vis,
label: Text(vis.text),
label: Text(vis.text(context)),
icon: Icon(vis.icon));
}).toList(),
onSelectionChanged: ((vset) {
@ -179,14 +197,15 @@ class _NewRoomPageState extends State {
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
if (_ctrID.text.length < 3) {
showSimpleSnackbar(scaffMgr,
text: _ctrID.text.isEmpty
? 'Please specify a Room ID'
: 'Room ID has to be at least three characters long',
action: 'OK');
? trans!.errorNoRoomId
: trans!.errorRoomIdLength,
action: trans.ok);
return;
}
@ -194,7 +213,7 @@ class _NewRoomPageState extends State {
// name may not be empty
if (_ctrName.text.isEmpty) {
showSimpleSnackbar(scaffMgr,
text: 'Please specify a room name', action: 'OK');
text: trans!.errorNoRoomName, action: trans.ok);
return;
}
@ -228,7 +247,7 @@ class _NewRoomPageState extends State {
router.pushReplacementNamed('home');
});
},
label: const Text('Create'),
label: Text(AppLocalizations.of(context)!.createRoomShort),
icon: const Icon(Icons.add)),
);
}

View file

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

View file

@ -1,5 +1,5 @@
import 'dart:math';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:outbag_app/backend/permissions.dart';
@ -62,15 +62,7 @@ class _EditRoomPermissionSetPageState extends State<EditRoomPermissionSetPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Default permissions'),
leading: IconButton(
onPressed: () {
// go back
Navigator.of(context).pop();
},
icon: const Icon(Icons.arrow_back),
tooltip: "Go back",
),
title: Text(AppLocalizations.of(context)!.roomDefaultPermissions),
),
body: ListView.builder(
itemCount: items.length,
@ -80,8 +72,8 @@ class _EditRoomPermissionSetPageState extends State<EditRoomPermissionSetPage> {
final int col = pow(2, index + 1) as int;
return SwitchListTile(
title: Text(RoomPermission.name(item)),
subtitle: Text(RoomPermission.describe(item)),
title: Text(RoomPermission.name(item, context)),
subtitle: Text(RoomPermission.describe(item, context)),
onChanged: (state) {
setState(() {
permissions += (state ? 1 : -1) * col;
@ -92,8 +84,8 @@ class _EditRoomPermissionSetPageState extends State<EditRoomPermissionSetPage> {
),
floatingActionButton: FloatingActionButton.extended(
icon: const Icon(Icons.edit),
tooltip: "Update default permission set",
label: const Text('Edit'),
tooltip: AppLocalizations.of(context)!.updateRoomPermissionsHint,
label: Text(AppLocalizations.of(context)!.updateRoomPermissions),
onPressed: () {
final router = GoRouter.of(context);
final sm = ScaffoldMessenger.of(context);

View file

@ -4,6 +4,7 @@ import 'package:outbag_app/backend/request.dart';
import 'package:outbag_app/backend/user.dart';
import 'package:outbag_app/tools/fetch_wrapper.dart';
import 'package:outbag_app/tools/snackbar.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
class ChangePasswordDialog extends StatefulWidget {
@ -21,7 +22,7 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('Change Password'),
title: Text(AppLocalizations.of(context)!.changeThemeTitle),
icon: const Icon(Icons.password),
content: SingleChildScrollView(
child: Column(
@ -32,13 +33,12 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
controller: _ctrOldPassword,
keyboardType: TextInputType.visiblePassword,
obscureText: true,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.lock),
labelText: 'Old Password',
hintText: 'Your current password',
helperText:
'For safety, you have to type your current passwort',
border: OutlineInputBorder(),
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(),
),
),
),
@ -48,12 +48,12 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
controller: _ctrNewPassword,
keyboardType: TextInputType.visiblePassword,
obscureText: true,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.lock),
labelText: 'New Password',
hintText: 'Your new password',
helperText: 'Password have to be at least six characters long',
border: OutlineInputBorder(),
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(),
),
),
),
@ -63,13 +63,12 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
controller: _ctrNewPasswordRepeat,
keyboardType: TextInputType.visiblePassword,
obscureText: true,
decoration: const InputDecoration(
prefixIcon: Icon(Icons.lock),
labelText: 'Repeat new Password',
hintText: 'Type your new password again',
helperText:
'Type your new password again, to make sure you know it',
border: OutlineInputBorder(),
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(),
),
),
),
@ -81,20 +80,21 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
// close popup
Navigator.of(context).pop();
},
child: const Text('Cancel'),
child: Text(AppLocalizations.of(context)!.cancel),
),
FilledButton(
onPressed: () async {
final scaffMgr = ScaffoldMessenger.of(context);
final nav = Navigator.of(context);
final user = context.read<User>();
final trans = AppLocalizations.of(context);
// validate password
if (_ctrNewPassword.text.length < 6) {
// password has to be at least 6 characters long
showSimpleSnackbar(scaffMgr,
text: 'Password has to be at least 6 characters longs',
action: 'Dismiss');
text: trans!.errorPasswordLength,
action: trans.dismiss);
_ctrNewPasswordRepeat.clear();
return;
@ -102,7 +102,7 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
if (_ctrNewPassword.text != _ctrNewPasswordRepeat.text) {
// new passwords do not match
showSimpleSnackbar(scaffMgr,
text: 'New passwords do not match', action: 'Dismiss');
text: trans!.errorPasswordsDoNotMatch, action: trans.dismiss);
_ctrNewPasswordRepeat.clear();
return;
@ -110,7 +110,7 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
if (hashPassword(_ctrOldPassword.text) != user.password) {
// current password wrong
showSimpleSnackbar(scaffMgr,
text: 'Old password is wrong', action: 'Dismiss');
text: trans!.errorOldPasswordWrong, action: trans.dismiss);
_ctrOldPassword.clear();
return;
@ -138,7 +138,7 @@ class _ChangePasswordDialogState extends State<ChangePasswordDialog> {
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/tools/fetch_wrapper.dart';
import 'package:provider/provider.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@ -26,7 +27,7 @@ class _SettingsPageState extends State<SettingsPage> {
return Scaffold(
appBar: AppBar(
title: const Text('Settings'),
title: Text(AppLocalizations.of(context)!.settings),
),
body: SingleChildScrollView(
child: Center(
@ -44,51 +45,58 @@ class _SettingsPageState extends State<SettingsPage> {
child: Text(user.humanReadable,
style: textTheme.titleLarge)),
ListTile(
title: const Text('Room count limit:'),
subtitle: const Text(
'How many rooms you are allowed to own'),
title: Text(
AppLocalizations.of(context)!.limitRoomCount),
subtitle: Text(AppLocalizations.of(context)!
.limitRoomCountSubtitle),
trailing: Text('${meta?.maxRoomCount ?? ""}'),
),
ListTile(
title: const Text('Room size limit:'),
subtitle: const Text(
'How many items/products/categories each room may contain'),
title: Text(
AppLocalizations.of(context)!.limitRoomSize),
subtitle: Text(AppLocalizations.of(context)!
.limitRoomSizeSubtitle),
trailing: Text('${meta?.maxRoomSize ?? ""}'),
),
ListTile(
title: const Text('Room member limit:'),
subtitle: const Text(
'How many members each of your rooms may have'),
title: Text(AppLocalizations.of(context)!
.limitRoomMemberCount),
subtitle: Text(AppLocalizations.of(context)!
.limitRoomMemberCountSubtitle),
trailing:
Text('${meta?.maxRoomMemberCount ?? ""}')),
ListTile(
title: const Text('Discoverable'),
subtitle: const Text(
'Determines if your account can be discovered by users from other servers'),
title: Text(AppLocalizations.of(context)!
.userDiscoverable),
subtitle: Text(AppLocalizations.of(context)!
.userDiscoverableSubtitle),
trailing: Checkbox(
tristate: true,
value: meta?.discoverable,
onChanged: (_) {},
onChanged: (_) {
// TODO: implement changeVisibility
},
))
],
)))),
// change theme button
ListTile(
title: const Text('Change Theme'),
subtitle: const Text(
'You can change between a light theme, a dark theme and automatic theme selection'),
title: Text(AppLocalizations.of(context)!.changeThemeTitle),
subtitle: Text(AppLocalizations.of(context)!.changeThemeSubtitle),
trailing: const Icon(Icons.chevron_right),
onTap: () {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Change Theme'),
title: Text(
AppLocalizations.of(context)!.changeThemeTitle),
content: SingleChildScrollView(
child: Column(children: [
const Padding(
padding: EdgeInsets.all(8),
child: Text('Choose your preferred theme'),
Padding(
padding: const EdgeInsets.all(8),
child: Text(AppLocalizations.of(context)!
.changeThemeSubtitle),
),
SegmentedButton<AppTheme>(
selected: {context.watch<AppTheme>()},
@ -100,7 +108,7 @@ class _SettingsPageState extends State<SettingsPage> {
return ButtonSegment<AppTheme>(
value: item,
icon: Icon(item.icon),
label: Text(item.name));
label: Text(item.name(context)));
}).toList(),
onSelectionChanged: (item) async {
try {
@ -111,9 +119,9 @@ class _SettingsPageState extends State<SettingsPage> {
])),
actions: [
FilledButton(
child: const Text('Close'),
child: Text(AppLocalizations.of(context)!.close),
onPressed: () {
Navigator.of(context).pop();
context.pop();
},
)
],
@ -123,8 +131,9 @@ class _SettingsPageState extends State<SettingsPage> {
// change password button
ListTile(
title: const Text('Change password'),
subtitle: const Text('Choose a new password for your account'),
title: Text(AppLocalizations.of(context)!.changePasswordTitle),
subtitle:
Text(AppLocalizations.of(context)!.changePasswordSubtitle),
onTap: () {
showDialog(
context: context,
@ -135,8 +144,8 @@ class _SettingsPageState extends State<SettingsPage> {
// export account to json
ListTile(
title: const Text('Export account'),
subtitle: const Text('Export account data'),
title: Text(AppLocalizations.of(context)!.exportAccountTitle),
subtitle: Text(AppLocalizations.of(context)!.exportAccountSubtitle),
onTap: () {
// TODO: show confirm dialog
// NOTE: json dump the localstore
@ -148,8 +157,8 @@ class _SettingsPageState extends State<SettingsPage> {
// delete account button
ListTile(
title: const Text('Delete account'),
subtitle: const Text('Delete your account from your homeserver'),
title: Text(AppLocalizations.of(context)!.deleteAccountTitle),
subtitle: Text(AppLocalizations.of(context)!.deleteAccountSubtitle),
onTap: () {
// show confirm dialog
// NOTE: same as logout
@ -158,16 +167,17 @@ class _SettingsPageState extends State<SettingsPage> {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Delete account'),
content: const Text(
'Do you really want to delete your account?'),
title: Text(
AppLocalizations.of(context)!.deleteAccountTitle),
content: Text(
AppLocalizations.of(context)!.deleteAccountConfirm),
actions: [
TextButton(
onPressed: () {
// close popup
Navigator.of(ctx).pop();
},
child: const Text('Cancel'),
child: Text(AppLocalizations.of(context)!.cancel),
),
FilledButton(
onPressed: () async {
@ -198,7 +208,7 @@ class _SettingsPageState extends State<SettingsPage> {
nav.pop();
});
},
child: const Text('Delete Account'),
child: Text(AppLocalizations.of(context)!.yes),
)
],
));
@ -210,22 +220,23 @@ class _SettingsPageState extends State<SettingsPage> {
Padding(
padding: const EdgeInsets.all(8),
child: FilledButton.tonal(
child: const Text('Log out'),
child: Text(AppLocalizations.of(context)!.logOut),
onPressed: () {
// show confirm dialog
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text('Log out'),
content:
const Text('Do you really want to log out?'),
title: Text(AppLocalizations.of(context)!.logOut),
content: Text(
AppLocalizations.of(context)!.logOutConfirm),
actions: [
TextButton(
onPressed: () {
// close popup
Navigator.of(ctx).pop();
},
child: const Text('Cancel'),
child:
Text(AppLocalizations.of(context)!.cancel),
),
FilledButton(
onPressed: () async {
@ -242,7 +253,7 @@ class _SettingsPageState extends State<SettingsPage> {
// go back 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:outbag_app/tools/assets.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'dart:math';
class WelcomePage extends StatefulWidget {
@ -33,17 +34,17 @@ class _WelcomePageState extends State<WelcomePage> {
.textTheme
.apply(displayColor: Theme.of(context).colorScheme.onSurface);
String fabText = "Next";
String fabText = AppLocalizations.of(context)!.next;
if (_currentPage == 0) {
fabText = "Let's go";
fabText = AppLocalizations.of(context)!.letsGo;
} 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) {
fabTooltip = "Take Tour";
fabTooltip = AppLocalizations.of(context)!.takeTour;
} else if (_currentPage == 4 - 1) {
fabTooltip = "Create an account";
fabTooltip = AppLocalizations.of(context)!.createNewAccount;
}
double width = MediaQuery.of(context).size.width;
@ -66,10 +67,13 @@ class _WelcomePageState extends State<WelcomePage> {
width: smallest * 0.5,
height: smallest * 0.5),
Text(
'Welcome to Outbag',
AppLocalizations.of(context)!.welcomeTitle,
style: textTheme.displaySmall,
),
Text('Shopping lists made easy', style: textTheme.bodyMedium)
Text(
AppLocalizations.of(context)!.welcomeSubtitle,
style: textTheme.bodyMedium
)
],
),
Column(
@ -81,10 +85,11 @@ class _WelcomePageState extends State<WelcomePage> {
width: smallest * 0.5,
height: smallest * 0.5),
Text(
'Open. Decentralized',
AppLocalizations.of(context)!.page2Title,
style: textTheme.displaySmall,
),
Text('One account, multiple servers',
Text(
AppLocalizations.of(context)!.page2Subtitle,
style: textTheme.bodyMedium)
],
),
@ -97,10 +102,11 @@ class _WelcomePageState extends State<WelcomePage> {
width: smallest * 0.5,
height: smallest * 0.5),
Text(
'Made to share',
AppLocalizations.of(context)!.page3Title,
style: textTheme.displaySmall,
),
Text('Collaborate on your shopping list in real time',
Text(
AppLocalizations.of(context)!.page3Subtitle,
style: textTheme.bodyMedium)
],
),
@ -113,10 +119,11 @@ class _WelcomePageState extends State<WelcomePage> {
width: smallest * 0.5,
height: smallest * 0.5),
Text(
'Pocket-sized',
AppLocalizations.of(context)!.page4Title,
style: textTheme.displaySmall,
),
Text('Always have your shopping list with you',
Text(
AppLocalizations.of(context)!.page4Subtitle,
style: textTheme.bodyMedium)
],
),
@ -126,7 +133,8 @@ class _WelcomePageState extends State<WelcomePage> {
onPressed: () {
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/request.dart';
import 'package:outbag_app/tools/snackbar.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
void doNetworkRequest(ScaffoldMessengerState? sm,
{required Future<Response> Function() req,
@ -11,6 +12,8 @@ void doNetworkRequest(ScaffoldMessengerState? sm,
Function()? onAnyErr,
Function()? after,
bool Function(Map<String, dynamic>)? onServerErr}) async {
AppLocalizations? trans = (sm!=null)?AppLocalizations.of(sm.context):null;
Response res;
try {
res = await req();
@ -23,7 +26,7 @@ void doNetworkRequest(ScaffoldMessengerState? sm,
}
if (showBar && sm != null) {
showSimpleSnackbar(sm, text: 'Network Error', action: 'Dismiss');
showSimpleSnackbar(sm, text: trans!.errorNetwork, action: trans.dismiss);
}
if (onAnyErr != null) {
onAnyErr();
@ -47,7 +50,7 @@ void doNetworkRequest(ScaffoldMessengerState? sm,
showBar = onServerErr(res.body);
}
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) {
onAnyErr();
@ -61,7 +64,7 @@ void doNetworkRequest(ScaffoldMessengerState? sm,
}
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"
source: hosted
version: "2.0.1"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_svg:
dependency: "direct main"
description:
@ -184,6 +189,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.0.15"
intl:
dependency: "direct main"
description:
name: intl
sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91"
url: "https://pub.dev"
source: hosted
version: "0.17.0"
js:
dependency: transitive
description:

View file

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