diff --git a/flutter_with_localapi_example.gif b/flutter_with_localapi_example.gif index fd918f4..e75466b 100644 Binary files a/flutter_with_localapi_example.gif and b/flutter_with_localapi_example.gif differ diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 18799d7..d56d632 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -17,6 +17,7 @@ "drawerDevice": "Device", "drawerDashboard": "Dashboard", "drawerLogout": "Logout", + "drawerPreferences": "Preferences", "alertDialogConfirm": "Ok", "usersFloatActionViewModule": "Change view module", "usersFloatActionAddUser": "Add new account", @@ -32,5 +33,6 @@ "usersDataTableHeaderAuthority": "Authority", "usersDataTableSearchbyName": "Search by name", "usersDataTableSearchbyEmail": "Search by SW email", - "usersDataTableSearchbyAuthority": "Search by Authority" + "usersDataTableSearchbyAuthority": "Search by Authority", + "preferencesSelectLanguage": "Select Language :" } \ No newline at end of file diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 6942dd6..7c00cd0 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -17,6 +17,7 @@ "drawerDevice": "設備", "drawerDashboard": "儀表版", "drawerLogout": "登出", + "drawerPreferences": "偏好設定", "alertDialogConfirm": "確定", "usersFloatActionViewModule": "切換模式", "usersFloatActionAddUser": "新增使用者帳號", @@ -32,5 +33,6 @@ "usersDataTableHeaderAuthority": "權限", "usersDataTableSearchbyName": "按姓名搜尋", "usersDataTableSearchbyEmail": "按電子信箱搜尋", - "usersDataTableSearchbyAuthority": "按權限搜尋" + "usersDataTableSearchbyAuthority": "按權限搜尋", + "preferencesSelectLanguage": "選擇語言 :" } \ No newline at end of file diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart new file mode 100755 index 0000000..70a3ec9 --- /dev/null +++ b/lib/l10n/l10n.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +class L10n { + static final all = [ + const Locale('en'), + const Locale('zh'), + ]; + static String getName(String code) { + switch (code) { + case 'zh': + return '繁體中文'; + case 'en': + default: + return 'English'; + } + } + + static String getCountryFlag(String code) { + // See https://en.wikipedia.org/wiki/Regional_indicator_symbol + switch (code) { + case 'zh': + return 'TW'; + case 'en': + default: + return 'US'; + } + } +} diff --git a/lib/localization/app_translations.dart b/lib/localization/app_translations.dart new file mode 100755 index 0000000..927c396 --- /dev/null +++ b/lib/localization/app_translations.dart @@ -0,0 +1,35 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show rootBundle; +import 'package:shared_preferences/shared_preferences.dart'; + +class AppTranslations { + Locale locale; + static Map? _localisedValues; + + AppTranslations(this.locale); + + static AppTranslations? of(BuildContext context) { + return Localizations.of(context, AppTranslations); + } + + static Future load(Locale locale) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + var languageCode = prefs.getString('languageCode'); + if (languageCode != null) { + locale = Locale(languageCode); + } + AppTranslations appTranslations = AppTranslations(locale); + String jsonContent = + await rootBundle.loadString("lib/l10n/app_${locale.languageCode}.arb"); + _localisedValues = json.decode(jsonContent); + return appTranslations; + } + + get currentLanguage => locale.languageCode; + + String text(String key) { + return _localisedValues![key] ?? "$key not found"; + } +} diff --git a/lib/localization/app_translations_delegate.dart b/lib/localization/app_translations_delegate.dart new file mode 100755 index 0000000..ececcaa --- /dev/null +++ b/lib/localization/app_translations_delegate.dart @@ -0,0 +1,25 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'app_translations.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class AppTranslationsDelegate extends LocalizationsDelegate { + final Locale newLocale; + + const AppTranslationsDelegate({required this.newLocale}); + + @override + bool isSupported(Locale locale) { + return AppLocalizations.supportedLocales.contains(locale); + } + + @override + Future load(Locale locale) { + return AppTranslations.load(newLocale); + } + + @override + bool shouldReload(LocalizationsDelegate old) { + return true; + } +} diff --git a/lib/localization/application.dart b/lib/localization/application.dart new file mode 100755 index 0000000..e6d3d55 --- /dev/null +++ b/lib/localization/application.dart @@ -0,0 +1,18 @@ +import 'dart:ui'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +class Application { + static final Application _application = Application._internal(); + + factory Application() => _application; + + Application._internal(); + + //returns the list of supported Locales + Iterable supportedLocales() => AppLocalizations.supportedLocales; + late LocaleChangeCallback onLocaleChanged; +} + +Application application = Application(); + +typedef LocaleChangeCallback = void Function(Locale locale); diff --git a/lib/localization/localization.dart b/lib/localization/localization.dart new file mode 100755 index 0000000..730ce43 --- /dev/null +++ b/lib/localization/localization.dart @@ -0,0 +1,3 @@ +export 'application.dart'; +export 'app_translations.dart'; +export 'app_translations_delegate.dart'; diff --git a/lib/main.dart b/lib/main.dart index 43a91f5..d556098 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:provider/provider.dart'; import 'package:url_strategy/url_strategy.dart'; +import 'localization/localization.dart'; import 'models/models.dart'; -import 'pages/pages.dart'; import 'route/route.dart'; Future main() async { @@ -26,12 +27,15 @@ class MyApp extends StatefulWidget { class _MyAppState extends State { late MyAppRouterDelegate delegate; late MyAppRouteInformationParser parser; + late AppTranslationsDelegate _newLocaleDelegate; @override void initState() { super.initState(); delegate = MyAppRouterDelegate(); parser = MyAppRouteInformationParser(); + _newLocaleDelegate = const AppTranslationsDelegate(newLocale: Locale("en")); + application.onLocaleChanged = onLocaleChange; } // This widget is the root of your application. @@ -56,9 +60,21 @@ class _MyAppState extends State { routerDelegate: delegate, routeInformationParser: parser, backButtonDispatcher: RootBackButtonDispatcher(), - localizationsDelegates: AppLocalizations.localizationsDelegates, + localizationsDelegates: [ + _newLocaleDelegate, + //provides localised strings + GlobalMaterialLocalizations.delegate, + //provides RTL support + GlobalWidgetsLocalizations.delegate, + ], supportedLocales: AppLocalizations.supportedLocales, ), ); } + + void onLocaleChange(Locale locale) { + setState(() { + _newLocaleDelegate = AppTranslationsDelegate(newLocale: locale); + }); + } } diff --git a/lib/models/pages.dart b/lib/models/pages.dart index 2048dea..59a2f3e 100644 --- a/lib/models/pages.dart +++ b/lib/models/pages.dart @@ -30,7 +30,11 @@ class LoginPage extends Page { class HomePage extends Page { final VoidCallback onLogout; final VoidCallback onUserList; - const HomePage({required this.onLogout, required this.onUserList}) + final VoidCallback onPreferences; + const HomePage( + {required this.onLogout, + required this.onUserList, + required this.onPreferences}) : super(key: const ValueKey('HomePage')); @override @@ -40,6 +44,7 @@ class HomePage extends Page { builder: (BuildContext context) => MyHomePage( onLogout: onLogout, onUserList: onUserList, + onPreferences: onPreferences, ), ); } @@ -48,7 +53,11 @@ class HomePage extends Page { class UserListPage extends Page { final VoidCallback onLogout; final VoidCallback onUserList; - const UserListPage({required this.onLogout, required this.onUserList}) + final VoidCallback onPreferences; + const UserListPage( + {required this.onLogout, + required this.onUserList, + required this.onPreferences}) : super(key: const ValueKey('UserListPage')); @override @@ -58,6 +67,30 @@ class UserListPage extends Page { builder: (BuildContext context) => UserList( onLogout: onLogout, onUserList: onUserList, + onPreferences: onPreferences, + ), + ); + } +} + +class PreferencesPage extends Page { + final VoidCallback onLogout; + final VoidCallback onUserList; + final VoidCallback onPreferences; + const PreferencesPage( + {required this.onLogout, + required this.onUserList, + required this.onPreferences}) + : super(key: const ValueKey('PreferencesPage')); + + @override + Route createRoute(BuildContext context) { + return MaterialPageRoute( + settings: this, + builder: (BuildContext context) => MyPreferences( + onLogout: onLogout, + onUserList: onUserList, + onPreferences: onPreferences, ), ); } diff --git a/lib/pages/drawer.dart b/lib/pages/drawer.dart index 95e1dba..ae2003d 100644 --- a/lib/pages/drawer.dart +++ b/lib/pages/drawer.dart @@ -1,20 +1,22 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import '../localization/localization.dart'; import '../models/models.dart'; import '../route/route.dart'; import 'pages.dart'; -enum DrawerIDs { home, users, devices, dashboard, logout } +enum DrawerIDs { home, users, devices, dashboard, logout, preferences } class MyDrawer extends StatefulWidget { final VoidCallback onLogout; final VoidCallback onUserList; + final VoidCallback onPreferences; const MyDrawer({ super.key, required this.onLogout, required this.onUserList, + required this.onPreferences, }); @override @@ -98,6 +100,10 @@ class _MyDrawerState extends State { }, ))); break; + case 5: + widget.onPreferences(); + MyAppRouterDelegate().loggedIn = true; + break; } } @@ -112,16 +118,26 @@ class _MyDrawerState extends State { ), )); drawerList.add(const Divider(height: 2, thickness: 2, color: Colors.white)); - drawerList.add(buildListTile(AppLocalizations.of(context)!.drawerHome, - DrawerIDs.home.index, Icons.home)); - drawerList.add(buildListTile(AppLocalizations.of(context)!.drawerUsers, - DrawerIDs.users.index, Icons.people)); - /*drawerList.add(buildListTile(AppLocalizations.of(context)!.drawerDevice, + drawerList.add(buildListTile( + AppTranslations.of(context)!.text('drawerHome'), + DrawerIDs.home.index, + Icons.home)); + drawerList.add(buildListTile( + AppTranslations.of(context)!.text('drawerUsers'), + DrawerIDs.users.index, + Icons.people)); + /*drawerList.add(buildListTile(AppTranslations.of(context)!.text('drawerDevice'), DrawerIDs.devices.index, Icons.devices)); - drawerList.add(buildListTile(AppLocalizations.of(context)!.drawerDashboard, + drawerList.add(buildListTile(AppTranslations.of(context)!.text('drawerDashboard'), DrawerIDs.dashboard.index, Icons.dashboard));*/ - drawerList.add(buildListTile(AppLocalizations.of(context)!.drawerLogout, - DrawerIDs.logout.index, Icons.logout)); + drawerList.add(buildListTile( + AppTranslations.of(context)!.text('drawerLogout'), + DrawerIDs.logout.index, + Icons.logout)); + drawerList.add(buildListTile( + AppTranslations.of(context)!.text('drawerPreferences'), + DrawerIDs.preferences.index, + Icons.settings)); } Widget buildListTile(String title, int index, IconData iconName) { diff --git a/lib/pages/expandable_fab.dart b/lib/pages/expandable_fab.dart index 531f3cb..72c29a4 100644 --- a/lib/pages/expandable_fab.dart +++ b/lib/pages/expandable_fab.dart @@ -1,6 +1,7 @@ import 'dart:math' as math; import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; + +import '../localization/localization.dart'; class ExpandableFab extends StatefulWidget { const ExpandableFab({ @@ -134,7 +135,8 @@ class _ExpandableFabState extends State child: FloatingActionButton( onPressed: _toggle, heroTag: "viewModule", - tooltip: AppLocalizations.of(context)!.usersFloatActionViewModule, + tooltip: + AppTranslations.of(context)!.text('usersFloatActionViewModule'), elevation: 0.0, backgroundColor: const Color.fromARGB(255, 102, 168, 223), hoverColor: Colors.orange, diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 8a4bbde..3172e83 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -2,17 +2,21 @@ import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; +import '../localization/localization.dart'; import '../models/models.dart'; import 'pages.dart'; class MyHomePage extends StatefulWidget { final VoidCallback onLogout; final VoidCallback onUserList; + final VoidCallback onPreferences; const MyHomePage( - {super.key, required this.onLogout, required this.onUserList}); + {super.key, + required this.onLogout, + required this.onUserList, + required this.onPreferences}); @override State createState() => _MyHomePageState(); @@ -60,7 +64,7 @@ class _MyHomePageState extends State { key: scaffoldKey, appBar: AppBar( centerTitle: true, - title: Text(AppLocalizations.of(context)!.homeAppBarTitle), + title: Text(AppTranslations.of(context)!.text('homeAppBarTitle')), leading: IconButton( icon: const Icon(Icons.menu), onPressed: () { @@ -71,6 +75,7 @@ class _MyHomePageState extends State { drawer: MyDrawer( onLogout: () => widget.onLogout(), onUserList: () => widget.onUserList(), + onPreferences: () => widget.onPreferences(), ), body: SafeArea( child: SingleChildScrollView( @@ -80,7 +85,7 @@ class _MyHomePageState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - 'Hi $_username,\n${AppLocalizations.of(context)!.welcomeHomePage}', + 'Hi $_username,\n${AppTranslations.of(context)!.text('welcomeHomePage')}', style: TextStyle( color: Colors.black.withOpacity(0.6), fontSize: 24, diff --git a/lib/pages/landing.dart b/lib/pages/landing.dart index 9743d65..4b8d293 100644 --- a/lib/pages/landing.dart +++ b/lib/pages/landing.dart @@ -1,10 +1,8 @@ // ignore_for_file: use_build_context_synchronously, prefer_const_constructors import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:provider/provider.dart'; -import '../models/models.dart'; +import '../localization/localization.dart'; class Landing extends StatefulWidget { const Landing({Key? key}) : super(key: key); @@ -32,7 +30,7 @@ class _LandingState extends State { const CircularProgressIndicator(), const Padding(padding: EdgeInsets.all(5.0)), Text( - AppLocalizations.of(context)!.loadingText, + AppTranslations.of(context)!.text('loadingText'), style: const TextStyle( fontSize: 24.0, color: Colors.white, diff --git a/lib/pages/login.dart b/lib/pages/login.dart index f00e42f..4c96e05 100644 --- a/lib/pages/login.dart +++ b/lib/pages/login.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; +import '../localization/localization.dart'; import '../models/models.dart'; import '../controllers/controllers.dart'; @@ -75,7 +75,8 @@ class _LoginState extends State { void _loginSubmitted() async { final FormState? form = _formKey.currentState; if (form == null || !form.validate()) { - showInSnackBar(AppLocalizations.of(context)!.loginButtonSubmitError); + showInSnackBar( + AppTranslations.of(context)!.text('oginButtonSubmitError')); } else { form.save(); var userBaseProvider = @@ -88,7 +89,8 @@ class _LoginState extends State { } catch (e, s) { print('Error: $e'); print('Stack: $s'); - showInSnackBar(AppLocalizations.of(context)!.loginButtonSubmitInvalid); + showInSnackBar( + AppTranslations.of(context)!.text('loginButtonSubmitInvalid')); } } } @@ -113,7 +115,7 @@ class _LoginState extends State { backgroundColor: Colors.transparent, appBar: AppBar( centerTitle: true, - title: Text(AppLocalizations.of(context)!.loginAppBarTitle), + title: Text(AppTranslations.of(context)!.text('loginAppBarTitle')), ), body: SafeArea( child: Form( @@ -132,7 +134,7 @@ class _LoginState extends State { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text( - AppLocalizations.of(context)!.welcomeHomePage, + AppTranslations.of(context)!.text('welcomeHomePage'), style: TextStyle( color: Colors.black.withOpacity(0.6), fontSize: 24, @@ -163,8 +165,8 @@ class _LoginState extends State { _loginSubmitted(); // Respond to button press }, icon: const Icon(Icons.login, size: 20), - label: - Text(AppLocalizations.of(context)!.loginAppBarTitle), + label: Text(AppTranslations.of(context)! + .text('loginAppBarTitle')), ), ], ), @@ -185,15 +187,15 @@ class _LoginState extends State { hintText == "Password" ? passwordController : usernameController, decoration: InputDecoration( hintText: hintText == 'Password' - ? AppLocalizations.of(context)!.loginCardPassword - : AppLocalizations.of(context)!.loginCardUserName, + ? AppTranslations.of(context)!.text('loginCardPassword') + : AppTranslations.of(context)!.text('loginCardUserName'), hintStyle: TextStyle(color: Colors.blueGrey[400]), prefixIcon: hintText == "Password" ? const Icon(Icons.lock) : const Icon(Icons.account_box), labelText: hintText == 'Password' - ? AppLocalizations.of(context)!.loginCardPassword - : AppLocalizations.of(context)!.loginCardUserName, + ? AppTranslations.of(context)!.text('loginCardPassword') + : AppTranslations.of(context)!.text('loginCardUserName'), labelStyle: TextStyle( color: Colors.grey[600], fontSize: 22, @@ -214,23 +216,24 @@ class _LoginState extends State { ) : null, counterText: hintText == "Password" - ? '${passwordController.text.length.toString()} ${AppLocalizations.of(context)!.loginCardUserNameCharacter}' + ? '${passwordController.text.length.toString()} ${AppTranslations.of(context)!.text('loginCardUserNameCharacter')}' : null, ), obscureText: hintText == "Password" ? _isHidden : false, validator: (String? value) { if (value != null && value.isEmpty) { return hintText == "Password" - ? AppLocalizations.of(context)!.loginCardPasswordRequired - : AppLocalizations.of(context)!.loginCardUserNameRequired; + ? AppTranslations.of(context)!.text('loginCardPasswordRequired') + : AppTranslations.of(context)!.text('loginCardUserNameRequired'); } else if (value != null && hintText == "Password" && value.length < 6) { - return AppLocalizations.of(context)!.loginCardPasswordLimitLength; + return AppTranslations.of(context)! + .text('loginCardPasswordLimitLength'); } else if (value != null && hintText == "Username" && !(value.contains("@"))) { - return AppLocalizations.of(context)!.loginCardUserNameCheck; + return AppTranslations.of(context)!.text('loginCardUserNameCheck'); } else { return null; } diff --git a/lib/pages/pages.dart b/lib/pages/pages.dart index 7006700..bd494c9 100644 --- a/lib/pages/pages.dart +++ b/lib/pages/pages.dart @@ -4,3 +4,4 @@ export 'login.dart'; export 'drawer.dart'; export 'users.dart'; export 'expandable_fab.dart'; +export 'preferences.dart'; diff --git a/lib/pages/preferences.dart b/lib/pages/preferences.dart new file mode 100644 index 0000000..0bebd49 --- /dev/null +++ b/lib/pages/preferences.dart @@ -0,0 +1,168 @@ +import 'package:flutter/material.dart'; +import 'package:restart_app/restart_app.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:thingsboard_app/l10n/l10n.dart'; + +import '../localization/localization.dart'; +import 'pages.dart'; + +class MyPreferences extends StatefulWidget { + final VoidCallback onLogout; + final VoidCallback onUserList; + final VoidCallback onPreferences; + const MyPreferences( + {super.key, + required this.onLogout, + required this.onUserList, + required this.onPreferences}); + + @override + State createState() => _MyPreferencesState(); +} + +class _MyPreferencesState extends State { + GlobalKey scaffoldKey = GlobalKey(); + late Locale? dropdownValue; + + @override + void initState() { + super.initState(); + _loadlanguageCode(); + } + + void _loadlanguageCode() async { + dropdownValue = const Locale('en'); + SharedPreferences prefs = await SharedPreferences.getInstance(); + String languageCode = prefs.getString('languageCode') ?? 'en'; + application.onLocaleChanged = onLocaleChange; + onLocaleChange(Locale(languageCode)); + dropdownValue = Locale(languageCode); + } + + void onLocaleChange(Locale locale) async { + setState(() { + AppTranslations.load(locale); + }); + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: const BoxDecoration( + image: DecorationImage( + alignment: Alignment.center, + image: AssetImage('assets/images/welcome_bg.png'), + fit: BoxFit.fill, + )), + child: Scaffold( + key: scaffoldKey, + backgroundColor: Colors.transparent, + appBar: AppBar( + centerTitle: true, + title: Text(AppTranslations.of(context)!.text('drawerPreferences')), + leading: IconButton( + icon: const Icon(Icons.menu), + onPressed: () { + scaffoldKey.currentState?.openDrawer(); + }, + ), + ), + drawer: MyDrawer( + onLogout: () => widget.onLogout(), + onUserList: () => widget.onUserList(), + onPreferences: () => widget.onPreferences(), + ), + body: SafeArea( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(10, 20, 10, 20), + child: Center( + child: Column( + children: [ + Text( + AppTranslations.of(context)! + .text('preferencesSelectLanguage'), + style: const TextStyle( + fontSize: 28, + color: Color.fromARGB(255, 223, 98, 49), + fontWeight: FontWeight.w600)), + const SizedBox(height: 20), + DropdownButton( + value: dropdownValue, + style: const TextStyle(fontSize: 26, color: Colors.white), + dropdownColor: const Color.fromARGB(255, 28, 163, 197), + icon: const Icon(Icons.language, + color: Color.fromARGB(255, 243, 100, 33), size: 32), + borderRadius: BorderRadius.circular(20), + items: L10n.all.map( + ((locale) { + final name = L10n.getName(locale.languageCode); + final countryCode = + L10n.getCountryFlag(locale.languageCode); + return DropdownMenuItem( + value: locale, + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _flagWidget(countryCode, context), + dropdownValue == locale + ? Text( + name, + style: const TextStyle( + color: Color.fromARGB( + 255, 250, 131, 34), + fontWeight: FontWeight.w600, + ), + ) + : Text(name), + ], + ), + )); + }), + ).toList(), + onChanged: (value) async { + //print('onChanged value:$value, dropdownValue:$dropdownValue'); + SharedPreferences prefs = + await SharedPreferences.getInstance(); + await prefs.setString('languageCode', value.toString()); + setState(() { + dropdownValue = Locale(value.toString()); + onLocaleChange(Locale(value.toString())); + }); + Restart.restartApp(); + }, + /*underline: Container( + height: 1, + color: const Color.fromARGB(255, 254, 255, 175)),*/ + ), + ], + ), + ), + ), + ), + ), + ); + } + + Widget _flagWidget(String countryCode, BuildContext context) { + final bool isRtl = Directionality.of(context) == TextDirection.rtl; + // 0x41 is Letter A + // 0x1F1E6 is Regional Indicator Symbol Letter A + // Example : + // firstLetter U => 20 + 0x1F1E6 + // secondLetter S => 18 + 0x1F1E6 + // See: https://en.wikipedia.org/wiki/Regional_Indicator_Symbol + final int firstLetter = countryCode.codeUnitAt(0) - 0x41 + 0x1F1E6; + final int secondLetter = countryCode.codeUnitAt(1) - 0x41 + 0x1F1E6; + var flagText = + String.fromCharCode(firstLetter) + String.fromCharCode(secondLetter); + return SizedBox( + // the conditional 50 prevents irregularities caused by the flags in RTL mode + width: isRtl ? 50 : null, + child: Text( + flagText, + style: const TextStyle(fontSize: 24), + ), + ); + } +} diff --git a/lib/pages/users.dart b/lib/pages/users.dart index d6823e6..510bce2 100644 --- a/lib/pages/users.dart +++ b/lib/pages/users.dart @@ -2,20 +2,22 @@ import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:thingsboard_app/controllers/user_base.dart'; import 'package:data_table_2/data_table_2.dart'; +import '../localization/localization.dart'; import '../models/models.dart'; import 'pages.dart'; class UserList extends StatefulWidget { final VoidCallback onLogout; final VoidCallback onUserList; + final VoidCallback onPreferences; const UserList({ super.key, required this.onLogout, required this.onUserList, + required this.onPreferences, }); @override @@ -98,7 +100,7 @@ class _UserListState extends State { key: scaffoldKey, appBar: AppBar( centerTitle: true, - title: Text(AppLocalizations.of(context)!.drawerUsers), + title: Text(AppTranslations.of(context)!.text('drawerUsers')), leading: IconButton( icon: const Icon(Icons.menu), onPressed: () { @@ -109,6 +111,7 @@ class _UserListState extends State { drawer: MyDrawer( onLogout: () => widget.onLogout(), onUserList: () => widget.onUserList(), + onPreferences: () => widget.onPreferences(), ), body: viewType == 'gridview' ? (columnCount == 2 @@ -151,7 +154,7 @@ class _UserListState extends State { children: [ FloatingActionButton( heroTag: "viewModule", - tooltip: AppLocalizations.of(context)!.usersFloatActionViewModule, + tooltip: AppTranslations.of(context)!.text('usersFloatActionViewModule'), elevation: 0.0, backgroundColor: const Color.fromARGB(255, 102, 168, 223), onPressed: () => changeMode(), @@ -164,7 +167,7 @@ class _UserListState extends State { ? FloatingActionButton( heroTag: "addUser", tooltip: - AppLocalizations.of(context)!.usersFloatActionAddUser, + AppTranslations.of(context)!.text('usersFloatActionAddUser'), elevation: 0.0, backgroundColor: const Color.fromARGB(255, 102, 168, 223), onPressed: () { @@ -434,7 +437,7 @@ class _UserListState extends State { context: context, builder: (BuildContext context) => AlertDialog( elevation: 0, - title: Text(AppLocalizations.of(context)!.usersUpdateDialogTitle, + title: Text(AppTranslations.of(context)!.text('usersUpdateDialogTitle'), textAlign: TextAlign.center), content: SingleChildScrollView( child: Column( @@ -442,46 +445,48 @@ class _UserListState extends State { TextField( controller: _userNameController, decoration: InputDecoration( - labelText: - AppLocalizations.of(context)!.usersUpdateDialogName, - hintText: - AppLocalizations.of(context)!.usersUpdateDialogName), + labelText: AppTranslations.of(context)! + .text('usersUpdateDialogName'), + hintText: AppTranslations.of(context)! + .text('usersUpdateDialogName')), ), TextField( controller: _emailController, decoration: InputDecoration( - labelText: - AppLocalizations.of(context)!.usersUpdateDialogEmail, - hintText: - AppLocalizations.of(context)!.usersUpdateDialogEmail), + labelText: AppTranslations.of(context)! + .text('usersUpdateDialogEmail'), + hintText: AppTranslations.of(context)! + .text('usersUpdateDialogEmail')), ), TextField( controller: _passwordController, decoration: InputDecoration( - labelText: - AppLocalizations.of(context)!.usersUpdateDialogPassword, - hintText: - AppLocalizations.of(context)!.usersUpdateDialogPassword), + labelText: AppTranslations.of(context)! + .text('usersUpdateDialogPassword'), + hintText: AppTranslations.of(context)! + .text('usersUpdateDialogPassword')), ), TextField( controller: _roleController, decoration: InputDecoration( - labelText: - AppLocalizations.of(context)!.usersUpdateDialogRole, - hintText: - AppLocalizations.of(context)!.usersUpdateDialogRole), + labelText: AppTranslations.of(context)! + .text('usersUpdateDialogRole'), + hintText: AppTranslations.of(context)! + .text('usersUpdateDialogRole')), ), ], )), actions: [ TextButton( - child: Text(AppLocalizations.of(context)!.usersUpdateDialogCancel), + child: Text( + AppTranslations.of(context)!.text('usersUpdateDialogCancel')), onPressed: () { Navigator.of(context).pop(); }, ), TextButton( - child: Text(AppLocalizations.of(context)!.usersUpdateDialogSave), + child: Text( + AppTranslations.of(context)!.text('usersUpdateDialogSave')), onPressed: () { setState(() { _userNameController.text; @@ -583,17 +588,21 @@ class _UserListState extends State { decoration: InputDecoration( prefixIcon: const Icon(Icons.search), labelText: searchType == 'email' - ? (AppLocalizations.of(context)!.usersDataTableSearchbyEmail) + ? (AppTranslations.of(context)! + .text('usersDataTableSearchbyEmail')) : (searchType == 'name' - ? AppLocalizations.of(context)!.usersDataTableSearchbyName - : AppLocalizations.of(context)! - .usersDataTableSearchbyAuthority), + ? AppTranslations.of(context)! + .text('usersDataTableSearchbyName') + : AppTranslations.of(context)! + .text('usersDataTableSearchbyAuthority')), hintText: searchType == 'email' - ? (AppLocalizations.of(context)!.usersDataTableSearchbyEmail) + ? (AppTranslations.of(context)! + .text('usersDataTableSearchbyEmail')) : (searchType == 'name' - ? AppLocalizations.of(context)!.usersDataTableSearchbyName - : AppLocalizations.of(context)! - .usersDataTableSearchbyAuthority), + ? AppTranslations.of(context)! + .text('usersDataTableSearchbyName') + : AppTranslations.of(context)! + .text('usersDataTableSearchbyAuthority')), border: const OutlineInputBorder( borderRadius: BorderRadius.all(Radius.circular(25.0))), suffixIcon: Row( @@ -622,8 +631,8 @@ class _UserListState extends State { PopupMenuItem( value: 'email', child: ListTile( - title: Text(AppLocalizations.of(context)! - .usersDataTableSearchbyEmail), + title: Text(AppTranslations.of(context)! + .text('usersDataTableSearchbyEmail')), textColor: searchType == 'email' ? const Color.fromARGB(255, 204, 47, 47) : Colors.white, @@ -632,8 +641,8 @@ class _UserListState extends State { PopupMenuItem( value: 'name', child: ListTile( - title: Text(AppLocalizations.of(context)! - .usersDataTableSearchbyName), + title: Text(AppTranslations.of(context)! + .text('usersDataTableSearchbyName')), textColor: searchType == 'name' ? const Color.fromARGB(255, 204, 47, 47) : Colors.white, @@ -642,8 +651,8 @@ class _UserListState extends State { PopupMenuItem( value: 'authority', child: ListTile( - title: Text(AppLocalizations.of(context)! - .usersDataTableSearchbyAuthority), + title: Text(AppTranslations.of(context)! + .text('usersDataTableSearchbyAuthority')), textColor: searchType == 'authority' ? const Color.fromARGB(255, 204, 47, 47) : Colors.white, @@ -688,9 +697,11 @@ class _UserListState extends State { List _createColumns() { return [ DataColumn( - label: Text(AppLocalizations.of(context)!.usersDataTableHeaderName)), + label: Text( + AppTranslations.of(context)!.text('usersDataTableHeaderName'))), DataColumn( - label: Text(AppLocalizations.of(context)!.usersDataTableHeaderEmail), + label: Text( + AppTranslations.of(context)!.text('usersDataTableHeaderEmail')), onSort: ((columnIndex, _) { setState(() { _currentSortColumn = columnIndex; @@ -704,8 +715,8 @@ class _UserListState extends State { }); })), DataColumn( - label: Text( - AppLocalizations.of(context)!.usersDataTableHeaderAuthority)), + label: Text(AppTranslations.of(context)! + .text('usersDataTableHeaderAuthority'))), ]; } diff --git a/lib/route/configuration.dart b/lib/route/configuration.dart index 741e9e3..1d7db3d 100644 --- a/lib/route/configuration.dart +++ b/lib/route/configuration.dart @@ -2,39 +2,71 @@ class MyAppRouteConfiguration { final bool unknown; final bool? loggedIn; final bool? onUserListPage; + final bool? onPreferencesPage; MyAppRouteConfiguration.unKnow() : unknown = true, loggedIn = null, - onUserListPage = null; + onUserListPage = null, + onPreferencesPage = null; bool get isUnKnow => - unknown == true && loggedIn == null && onUserListPage == null; + unknown == true && + loggedIn == null && + onUserListPage == null && + onPreferencesPage == null; MyAppRouteConfiguration.landing() : unknown = false, loggedIn = null, - onUserListPage = null; + onUserListPage = null, + onPreferencesPage = null; bool get isLanding => - unknown == false && loggedIn == null && onUserListPage == null; + unknown == false && + loggedIn == null && + onUserListPage == null && + onPreferencesPage == null; MyAppRouteConfiguration.login() : unknown = false, loggedIn = false, - onUserListPage = null; + onUserListPage = null, + onPreferencesPage = null; bool get isLoginPage => - unknown == false && loggedIn == false && onUserListPage == null; + unknown == false && + loggedIn == false && + onUserListPage == null && + onPreferencesPage == null; MyAppRouteConfiguration.home() : unknown = false, loggedIn = true, - onUserListPage = null; + onUserListPage = null, + onPreferencesPage = null; bool get isHomePage => - unknown == false && loggedIn == true && onUserListPage == null; + unknown == false && + loggedIn == true && + onUserListPage == null && + onPreferencesPage == null; MyAppRouteConfiguration.userList() : unknown = false, loggedIn = true, - onUserListPage = true; + onUserListPage = true, + onPreferencesPage = null; bool get isUserListPage => - unknown == false && loggedIn == true && onUserListPage == true; + unknown == false && + loggedIn == true && + onUserListPage == true && + onPreferencesPage == null; + + MyAppRouteConfiguration.preferences() + : unknown = false, + loggedIn = true, + onUserListPage = null, + onPreferencesPage = true; + bool get isPreferencesPage => + unknown == false && + loggedIn == true && + onUserListPage == null && + onPreferencesPage == true; } diff --git a/lib/route/delegate.dart b/lib/route/delegate.dart index 6ff9ae7..67a0887 100644 --- a/lib/route/delegate.dart +++ b/lib/route/delegate.dart @@ -44,9 +44,17 @@ class MyAppRouterDelegate extends RouterDelegate notifyListeners(); } + bool? _preferencesIn; + bool? get preferencesIn => _preferencesIn; + set preferencesIn(bool? value) { + _preferencesIn = value; + notifyListeners(); + } + _clear() { show404 = null; userListIn = null; + preferencesIn = null; } List get _landingStack => [const LandingPage()]; @@ -65,15 +73,33 @@ class MyAppRouterDelegate extends RouterDelegate onUserList() { userListIn = true; + preferencesIn = null; + } + + onPreferences() { + userListIn = null; + preferencesIn = true; } return [ - HomePage(onLogout: onLogout, onUserList: onUserList), + HomePage( + onLogout: onLogout, + onUserList: onUserList, + onPreferences: onPreferences, + ), if (userListIn != null) if (userListIn!) UserListPage( onLogout: onLogout, onUserList: onUserList, + onPreferences: onPreferences, + ), + if (preferencesIn != null) + if (preferencesIn!) + PreferencesPage( + onLogout: onLogout, + onUserList: onUserList, + onPreferences: onPreferences, ), ]; } @@ -108,8 +134,13 @@ class MyAppRouterDelegate extends RouterDelegate if (userListIn!) { userListIn = null; } + } else if (preferencesIn != null) { + if (preferencesIn!) { + preferencesIn = null; + } } else { userListIn = null; + preferencesIn = null; } return true; }, @@ -128,6 +159,8 @@ class MyAppRouterDelegate extends RouterDelegate return MyAppRouteConfiguration.home(); } else if (loggedIn == true) { return MyAppRouteConfiguration.userList(); + } else if (preferencesIn == true) { + return MyAppRouteConfiguration.preferences(); } else { return null; } @@ -142,9 +175,15 @@ class MyAppRouterDelegate extends RouterDelegate configuration.isLoginPage) { show404 = false; userListIn = null; + preferencesIn = null; } else if (configuration.isUserListPage) { show404 = false; userListIn = true; + preferencesIn = null; + } else if (configuration.isUserListPage) { + show404 = false; + userListIn = null; + preferencesIn = true; } else { print('setNewRoutePath: Could not set new route'); } diff --git a/lib/route/information_parser.dart b/lib/route/information_parser.dart index e8fe0c9..b5318a9 100644 --- a/lib/route/information_parser.dart +++ b/lib/route/information_parser.dart @@ -19,6 +19,8 @@ class MyAppRouteInformationParser return MyAppRouteConfiguration.login(); case 'users': return MyAppRouteConfiguration.userList(); + case 'preferences': + return MyAppRouteConfiguration.preferences(); default: return MyAppRouteConfiguration.unKnow(); } @@ -40,6 +42,8 @@ class MyAppRouteInformationParser return const RouteInformation(location: '/home'); } else if (configuration.isUserListPage) { return const RouteInformation(location: '/users'); + } else if (configuration.isPreferencesPage) { + return const RouteInformation(location: '/preferences'); } else { return null; } diff --git a/pubspec.lock b/pubspec.lock index a2ab97b..8771cf6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -497,7 +497,7 @@ packages: name: plugin_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "2.1.2" + version: "2.1.3" pool: dependency: transitive description: @@ -533,6 +533,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.1" + restart_app: + dependency: "direct main" + description: + name: restart_app + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" shared_preferences: dependency: "direct main" description: @@ -763,5 +770,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.18.0 <3.0.0" + dart: ">=2.18.4 <3.0.0" flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 9796da9..37faf1b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,6 +52,7 @@ dependencies: path: ^1.8.2 http: ^0.13.5 data_table_2: ^2.3.8 + restart_app: ^1.1.1 dev_dependencies: flutter_driver: @@ -89,6 +90,7 @@ flutter: assets: - assets/images/ - .env + - lib/l10n/ # An image asset can refer to one or more resolution-specific "variants", see # https://flutter.dev/assets-and-images/#resolution-aware