diff --git a/mobile/Dockerfile b/mobile/Dockerfile index 5fd4bc0..7bc2bea 100644 --- a/mobile/Dockerfile +++ b/mobile/Dockerfile @@ -11,5 +11,7 @@ COPY pubspec.* ./ RUN flutter pub get COPY . . +# Generate traduction files +RUN fkutter gen-l10n RUN flutter build apk lib/src/main.dart CMD mv ./build/app/outputs/flutter-apk/app-release.apk /dist/aeris_android.apk diff --git a/mobile/I10n.yaml b/mobile/I10n.yaml index bb557b3..15338f2 100644 --- a/mobile/I10n.yaml +++ b/mobile/I10n.yaml @@ -1,3 +1,3 @@ arb-dir: lib/l10n template-arb-file: app_en.arb -output-localization-file: app_localizations.dartà \ No newline at end of file +output-localization-file: app_localizations.dart diff --git a/mobile/lib/I10n/app_en.arb b/mobile/lib/I10n/app_en.arb deleted file mode 100644 index 4f466e2..0000000 --- a/mobile/lib/I10n/app_en.arb +++ /dev/null @@ -1,6 +0,0 @@ -{ - "helloWorld": "Hello World!", - "@helloWorld": { - "description": "The conventional newborn programmer greeting" - } -} \ No newline at end of file diff --git a/mobile/lib/I10n/app_fr.arb b/mobile/lib/I10n/app_fr.arb deleted file mode 100644 index 6bf8554..0000000 --- a/mobile/lib/I10n/app_fr.arb +++ /dev/null @@ -1,3 +0,0 @@ -{ - "helloWorld": "Bonjour, monde!", -} \ No newline at end of file diff --git a/mobile/lib/l10n/app_en.arb b/mobile/lib/l10n/app_en.arb new file mode 100644 index 0000000..92e5cb3 --- /dev/null +++ b/mobile/lib/l10n/app_en.arb @@ -0,0 +1,29 @@ +{ + "helloWorld": "Hello World!", + "lastTrigger": "Last", + "never": "Never", + "nDaysAgo": "n days ago", + "today": "Today", + "nameOfThePipeline": "Name of the pipeline", + "addReaction": "Add a Reaction", + "addTrigger": "Add a Trigger", + "modify": "Modify", + "delete": "Delete", + "services": "Services", + "logout": "Logout", + "usernameOrPasswordIncorrect": "Username or password is incorrect", + "userDoesNotExist": "User does not exist", + "enabled": "Enabled", + "disabled": "Disabled", + "disconnect": "Disconnect", + "deletePipeline": "Delete Pipeline", + "connected": "Connected", + "connect": "Connect", + "available": "Available", + "disconnectServiceWarningMessage": "You are about to disconnect to a service. Once disconnected, every related pipeline will be deleted. This action cannot be undone.", + "deletePipelineWarningMessage": "You are about to delete a pipeline. This action can not be undone. Are you sure ?", + "avalableActionsFor": "available actions for", + "mergeDisabledPipelines": "Merge disabled pipelines", + "seperateDisabledPipelines": "Seperate disabled pipelines", + "aerisDescription": "Aeris is the best AREA in Nantes! Control each of your social network with Aeris, your new Action / Reaction app." +} \ No newline at end of file diff --git a/mobile/lib/l10n/app_fr.arb b/mobile/lib/l10n/app_fr.arb new file mode 100644 index 0000000..6fe4ca0 --- /dev/null +++ b/mobile/lib/l10n/app_fr.arb @@ -0,0 +1,29 @@ +{ + "helloWorld": "Bonjour le monde!", + "lastTrigger": "Dernière", + "never": "Jamais", + "nDaysAgo": " jours", + "today": "Aujourd'hui", + "nameOfThePipeline": "Nom de la pipeline", + "addReaction": "Ajouter une Reaction", + "addTrigger": "Ajouter un Déclancheur", + "modify": "Modifier", + "delete": "Supprimer", + "services": "Services", + "logout": "Se Deconnecter", + "usernameOrPasswordIncorrect": "Nom d'utilisateur ou mot de passe incorrect", + "userDoesNotExist": "L'utilisateur n'existe pas", + "enabled": "Activé", + "disabled": "Désactivé", + "disconnect": "Déconnecter", + "deletePipeline": "Supprimer la Pipeline", + "connected": "Connecté", + "connect": "Se Connecter", + "available": "Disponible", + "disconnectServiceWarningMessage": "Vous allez supprimer un service. Une fois fait, toutes les pipelines associées seront supprimées. Cela ne peut pas être annulé.", + "deletePipelineWarningMessage": "Vous allez supprimer une pipeline. Cela ne peut pas être annulé.", + "avalableActionsFor": "Actions disponibles pour", + "mergeDisabledPipelines": "Mélanger les pipelines désactivées", + "seperateDisabledPipelines": "Séparer les pipelines désactivées", + "aerisDescription": "Aeris est le meilleur AREA de Nantes! Prennez le contrôle de vos réseaux sociaux avec Aeris, la nouvelle application de pipeline!" +} \ No newline at end of file diff --git a/mobile/lib/src/main.dart b/mobile/lib/src/main.dart index d72b680..7a343ab 100644 --- a/mobile/lib/src/main.dart +++ b/mobile/lib/src/main.dart @@ -12,35 +12,35 @@ import 'package:mobile/src/views/home_page.dart'; import 'package:mobile/src/constants.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; void main() { - runApp( - MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => PipelineProvider()), - ChangeNotifierProvider(create: (_) => UserServiceProvider()) - ], - child: const MyApp() - ) - ); + runApp(MultiProvider(providers: [ + ChangeNotifierProvider(create: (_) => PipelineProvider()), + ChangeNotifierProvider(create: (_) => UserServiceProvider()) + ], child: const Aeris())); } -class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); +class Aeris extends StatelessWidget { + static GlobalKey materialKey = GlobalKey(); + const Aeris({Key? key}) : super(key: key); ///This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( + locale: const Locale('fr', ''), + navigatorKey: Aeris.materialKey, debugShowCheckedModeBanner: false, title: 'Aeris', localizationsDelegates: const [ + AppLocalizations.delegate, GlobalMaterialLocalizations.delegate, GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, FormBuilderLocalizations.delegate ], - supportedLocales: const [Locale('en', ''), Locale('fr', '')], + supportedLocales: const [Locale('fr', ''), Locale('en', '')], theme: ThemeData(colorScheme: aerisScheme), initialRoute: '/', onGenerateRoute: (settings) { @@ -60,23 +60,25 @@ class MyApp extends StatelessWidget { ..addAll(cardRoutes) ..addAll(pageRoutes); return PageRouteBuilder( - opaque: false, - settings: settings, - pageBuilder: (_, __, ___) => routes[settings.name].call(), - transitionDuration: const Duration(milliseconds: 350), - transitionsBuilder: (context, animation, secondaryAnimation, child) => - pageRoutes.containsKey(settings.name) ? ScaleTransition( - child: child, - scale: CurvedAnimation( - parent: animation, - curve: Curves.ease, - ) - ) : SlideTransition( - position: animation.drive(Tween(begin: const Offset(0, 1), end: Offset.zero).chain(CurveTween(curve: Curves.ease))), - child: child, - ) - ); - } - ); + opaque: false, + settings: settings, + pageBuilder: (_, __, ___) => routes[settings.name].call(), + transitionDuration: const Duration(milliseconds: 350), + transitionsBuilder: (context, animation, secondaryAnimation, + child) => + pageRoutes.containsKey(settings.name) + ? ScaleTransition( + child: child, + scale: CurvedAnimation( + parent: animation, + curve: Curves.ease, + )) + : SlideTransition( + position: animation.drive( + Tween(begin: const Offset(0, 1), end: Offset.zero) + .chain(CurveTween(curve: Curves.ease))), + child: child, + )); + }); } } diff --git a/mobile/lib/src/models/action.dart b/mobile/lib/src/models/action.dart index e1f5efe..4d34a12 100644 --- a/mobile/lib/src/models/action.dart +++ b/mobile/lib/src/models/action.dart @@ -10,7 +10,7 @@ abstract class Action { String name; ///Action's parameters - Map parameters; + Map parameters; Action( {Key? key, required this.service, diff --git a/mobile/lib/src/models/reaction.dart b/mobile/lib/src/models/reaction.dart index 496d911..5dde37d 100644 --- a/mobile/lib/src/models/reaction.dart +++ b/mobile/lib/src/models/reaction.dart @@ -8,7 +8,7 @@ class Reaction extends aeris_action.Action { {Key? key, required Service service, required String name, - Map parameters = const {}}) + Map parameters = const {}}) : super(service: service, name: name, parameters: parameters); /// Template trigger, used as an 'empty' trigger diff --git a/mobile/lib/src/models/trigger.dart b/mobile/lib/src/models/trigger.dart index e25eaaf..c07273a 100644 --- a/mobile/lib/src/models/trigger.dart +++ b/mobile/lib/src/models/trigger.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:mobile/src/main.dart'; import 'package:mobile/src/models/service.dart'; import 'package:mobile/src/models/action.dart' as aeris_action; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; ///Object representation of a pipeline trigger class Trigger extends aeris_action.Action { @@ -10,17 +12,20 @@ class Trigger extends aeris_action.Action { {Key? key, required Service service, required String name, - Map parameters = const {}, + Map parameters = const {}, this.last}) : super(service: service, name: name, parameters: parameters); ///TODO Constructor from DB 'Type' field + ///TODO translate String lastToString() { - if (last == null) return 'Last: Never'; + var context = AppLocalizations.of(Aeris.materialKey.currentContext!); + String lastStr = context.lastTrigger; + if (last == null) return '$lastStr: ${context.lastTrigger}'; int elapsedDays = DateTime.now().difference(last!).inDays; return elapsedDays == 0 - ? 'Last: Today' - : 'Last: ${elapsedDays.toString()}d ago'; + ? '$lastStr: ${context.today}' + : '$lastStr: $elapsedDays${context.nDaysAgo}'; } /// Template trigger, used as an 'empty' trigger diff --git a/mobile/lib/src/views/create_pipeline_page.dart b/mobile/lib/src/views/create_pipeline_page.dart index 5f545b3..9c67822 100644 --- a/mobile/lib/src/views/create_pipeline_page.dart +++ b/mobile/lib/src/views/create_pipeline_page.dart @@ -12,6 +12,7 @@ import 'package:mobile/src/widgets/aeris_card_page.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:mobile/src/widgets/colored_clickable_card.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; /// Page to create a new pipeline class CreatePipelinePage extends StatefulWidget { @@ -22,7 +23,6 @@ class CreatePipelinePage extends StatefulWidget { } class _CreatePipelinePageState extends State { - /// Creates a basic Template the user can modify Trigger trigger = Trigger.template(); @@ -36,113 +36,126 @@ class _CreatePipelinePageState extends State { Widget build(BuildContext context) { final _formKey = GlobalKey(); return Consumer( - builder: (context, provider, _) => - AerisCardPage( - body: ListView(children: [ - const Text("Create a new pipeline", - style: TextStyle( - fontSize: 25, - ) - ), - FormBuilder( - key: _formKey, - child: Padding( - padding: const EdgeInsets.all(20), - child: Column(children: [ - FormBuilderTextField( - name: 'name', - initialValue: name, - decoration: const InputDecoration( - labelText: 'Name of the pipeline', - ), - validator: FormBuilderValidators.compose([ - FormBuilderValidators.required(context), - FormBuilderValidators.minLength(context, 5), + builder: (context, provider, _) => AerisCardPage( + body: ListView(children: [ + const Text("Create a new pipeline", + style: TextStyle( + fontSize: 25, + )), + FormBuilder( + key: _formKey, + child: Padding( + padding: const EdgeInsets.all(20), + child: Column(children: [ + FormBuilderTextField( + name: 'name', + initialValue: name, + decoration: InputDecoration( + labelText: + AppLocalizations.of(context).nameOfThePipeline, + ), + validator: FormBuilderValidators.compose([ + FormBuilderValidators.required(context), + FormBuilderValidators.minLength(context, 5), + ]), + onChanged: (value) { + name = value; + }, + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: trigger == Trigger.template() + ? ColoredClickableCard( + color: Theme.of(context) + .colorScheme + .secondaryContainer, + text: AppLocalizations.of(context).addTrigger, + onTap: () { + print("add trigger"); // TODO add reaction + Navigator.of(context) + .pushNamed('/pipeline/action/new', + arguments: + SetupActionPageArguments(trigger)) + .then((_) => setState(() {})); + }) + : ActionCard( + leading: trigger.service.getLogo(logoSize: 50), + title: trigger.service.name, + trailing: ActionCardPopupMenu( + deletable: false, + action: trigger, + then: () => setState(() {})), + ), + ), + ...[ + for (Reaction reaction in reactions) + ActionCard( + leading: reaction.service.getLogo(logoSize: 50), + title: reaction.service.name, + trailing: ActionCardPopupMenu( + deletable: reaction != reactions[0], + action: reaction, + then: () => setState(() {}), + onDelete: () { + setState(() { + reactions.remove(reaction); + }); + })) + ], + Padding( + padding: const EdgeInsets.all(8.0), + child: ColoredClickableCard( + color: Theme.of(context) + .colorScheme + .secondaryContainer, + text: AppLocalizations.of(context).addReaction, + onTap: () async { + // TODO add to db + reactions.add(Reaction.template()); + await Navigator.of(context).pushNamed( + '/pipeline/action/new', + arguments: + SetupActionPageArguments(reactions.last)); + setState(() {}); + }), + ), + ElevatedButton( + child: const Text("Save"), + onPressed: () { + _formKey.currentState!.save(); + if (_formKey.currentState!.validate()) { + if (trigger == Trigger.template() || + reactions.isEmpty || + reactions + .where((element) => + element == Reaction.template()) + .isNotEmpty) { + ScaffoldMessenger.of(context).showSnackBar(SnackBar( + backgroundColor: + Theme.of(context).colorScheme.secondary, + content: const Text( + "You must select at least a trigger and a reaction"))); + } else { + Pipeline newPipeline = Pipeline( + id: 0, + name: _formKey.currentState!.value['name'], + triggerCount: 0, + enabled: true, + parameters: {}, + trigger: trigger, + reactions: reactions); + provider.addPipelineInProvider(newPipeline); + + ///TODO add to db + Navigator.of(context).popAndPushNamed('/pipeline', + arguments: + PipelineDetailPageArguments(newPipeline)); + } + } + }, + ), ]), - onChanged: (value) { - name = value; - }, - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: trigger == Trigger.template() ? ColoredClickableCard( - color: Theme.of(context).colorScheme.secondaryContainer, - text: "Add Trigger", - onTap: () { - print("add trigger"); // TODO add reaction - Navigator.of(context).pushNamed('/pipeline/action/new', - arguments: SetupActionPageArguments(trigger) - ).then((_) => setState(() {})); - } - ) : ActionCard( - leading: trigger.service.getLogo(logoSize: 50), - title: trigger.service.name, - trailing: ActionCardPopupMenu( - deletable: false, - action: trigger, - then: () => setState(() {})), - ), - ), - ...[ - for (Reaction reaction in reactions) - ActionCard( - leading: reaction.service.getLogo(logoSize: 50), - title: reaction.service.name, - trailing: ActionCardPopupMenu( - deletable: reaction != reactions[0], - action: reaction, - then: () => setState(() {})), - ) - ], - Padding( - padding: const EdgeInsets.all(8.0), - child: ColoredClickableCard( - color: Theme.of(context).colorScheme.secondaryContainer, - text: "Add Reaction", - onTap: () async { - // TODO add to db - reactions.add(Reaction.template()); - await Navigator.of(context).pushNamed('/pipeline/action/new', - arguments: SetupActionPageArguments(reactions.last) - ); - setState(() {}); - } - ), - ), - ElevatedButton( - child: const Text("Save"), - onPressed: () { - _formKey.currentState!.save(); - if (_formKey.currentState!.validate()) { - if (trigger == Trigger.template() || reactions.isEmpty || - reactions.where((element) => element == Reaction.template()).isNotEmpty) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - backgroundColor: Theme.of(context).colorScheme.secondary, - content: const Text("You must select at least a trigger and a reaction")) - ); - } else { - Pipeline newPipeline = Pipeline( - id: 0, - name: _formKey.currentState!.value['name'], - triggerCount: 0, - enabled: true, - parameters: {}, - trigger: trigger, - reactions: reactions - ); - provider.addPipelineInProvider(newPipeline); - ///TODO add to db - Navigator.of(context).popAndPushNamed('/pipeline', - arguments: PipelineDetailPageArguments(newPipeline) - ); - } - } - }, - ), - ]), - )), - ]) - ) - ); + )), + ]))); } } diff --git a/mobile/lib/src/views/login_page.dart b/mobile/lib/src/views/login_page.dart index 411b00e..221e5b1 100644 --- a/mobile/lib/src/views/login_page.dart +++ b/mobile/lib/src/views/login_page.dart @@ -1,6 +1,8 @@ +import 'package:mobile/src/main.dart'; import 'package:mobile/src/widgets/aeris_page.dart'; import 'package:flutter_login/flutter_login.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; const users = { 'dribbble@gmail.com': '12345', @@ -19,10 +21,12 @@ class LoginPage extends StatelessWidget { debugPrint('Name: ${data.name}, Password: ${data.password}'); return Future.delayed(loginDuration).then((_) { if (!users.containsKey(data.name)) { - return 'User does not exists'; + return AppLocalizations.of(Aeris.materialKey.currentContext!) + .usernameOrPasswordIncorrect; } if (users[data.name] != data.password) { - return 'Password does not match'; + return AppLocalizations.of(Aeris.materialKey.currentContext!) + .usernameOrPasswordIncorrect; } return null; }); @@ -41,7 +45,7 @@ class LoginPage extends StatelessWidget { debugPrint('Name: $name'); return Future.delayed(loginDuration).then((_) { if (!users.containsKey(name)) { - return 'User does not exists'; + return AppLocalizations.of(Aeris.materialKey.currentContext!).userDoesNotExist; } return null; }); @@ -50,23 +54,20 @@ class LoginPage extends StatelessWidget { @override Widget build(BuildContext context) { return AerisPage( - displayAppbar: false, - body: FlutterLogin( - disableCustomPageTransformer: true, - logo: const AssetImage("assets/logo.png"), - onRecoverPassword: _recoverPassword, - theme: LoginTheme( - pageColorLight: Colors.transparent, - pageColorDark: Colors.transparent, - primaryColor: Theme.of(context).colorScheme.primary - ), - onLogin: _authUser, - onSignup: _signupUser, - onSubmitAnimationCompleted: () { - Navigator.of(context).popUntil((route) => route.isFirst); - Navigator.of(context).popAndPushNamed("/home"); - } - ) - ); + displayAppbar: false, + body: FlutterLogin( + disableCustomPageTransformer: true, + logo: const AssetImage("assets/logo.png"), + onRecoverPassword: _recoverPassword, + theme: LoginTheme( + pageColorLight: Colors.transparent, + pageColorDark: Colors.transparent, + primaryColor: Theme.of(context).colorScheme.primary), + onLogin: _authUser, + onSignup: _signupUser, + onSubmitAnimationCompleted: () { + Navigator.of(context).popUntil((route) => route.isFirst); + Navigator.of(context).popAndPushNamed("/home"); + })); } } diff --git a/mobile/lib/src/views/pipeline_detail_page.dart b/mobile/lib/src/views/pipeline_detail_page.dart index 594707a..3796d9a 100644 --- a/mobile/lib/src/views/pipeline_detail_page.dart +++ b/mobile/lib/src/views/pipeline_detail_page.dart @@ -1,3 +1,4 @@ +import 'package:mobile/src/main.dart'; import 'package:mobile/src/providers/pipelines_provider.dart'; import 'package:mobile/src/views/setup_action_page.dart'; import 'package:mobile/src/widgets/action_card_popup_menu.dart'; @@ -10,6 +11,7 @@ import 'package:mobile/src/models/reaction.dart'; import 'package:mobile/src/models/pipeline.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; /// Class to get the pipeline's name in route's arguments class PipelineDetailPageArguments { @@ -33,7 +35,9 @@ class _PipelineDetailPageState extends State { @override Widget build(BuildContext context) => Consumer(builder: (context, provider, _) { - final PipelineDetailPageArguments arguments = ModalRoute.of(context)!.settings.arguments as PipelineDetailPageArguments; + final PipelineDetailPageArguments arguments = ModalRoute.of(context)! + .settings + .arguments as PipelineDetailPageArguments; Pipeline pipeline = arguments.pipeline; final cardHeader = Row( @@ -45,139 +49,129 @@ class _PipelineDetailPageState extends State { Align( alignment: Alignment.centerLeft, child: Text(pipeline.name, - style: const TextStyle( - fontSize: 25, - ) - ), + style: const TextStyle( + fontSize: 25, + )), ), const SizedBox(height: 10), Align( alignment: Alignment.centerLeft, child: Text(pipeline.trigger.lastToString(), - style: const TextStyle( - fontSize: 17, - ) - ), + style: const TextStyle( + fontSize: 17, + )), ), ], ), ), Expanded( - flex: 3, - child: Column( - children: [ - const SizedBox(height: 10), - Align( - alignment: Alignment.center, - child: FlutterSwitch( - activeColor: Colors.green, - width: 60, - value: pipeline.enabled, - onToggle: (value) { - setState(() { - pipeline.enabled = !pipeline.enabled; - provider.sortPipelines(); - provider.notifyListeners(); - // TODO call api - }); - }, + flex: 3, + child: Column( + children: [ + const SizedBox(height: 10), + Align( + alignment: Alignment.center, + child: FlutterSwitch( + activeColor: Colors.green, + width: 60, + value: pipeline.enabled, + onToggle: (value) { + setState(() { + pipeline.enabled = !pipeline.enabled; + provider.sortPipelines(); + provider.notifyListeners(); + // TODO call api + }); + }, + ), ), - ), - const SizedBox(height: 10), - Align( - alignment: Alignment.center, - child: Text(pipeline.enabled ? "Enabled" : "Disabed", - style: const TextStyle(fontSize: 13) + const SizedBox(height: 10), + Align( + alignment: Alignment.center, + child: Text( + pipeline.enabled + ? AppLocalizations.of(context).enabled + : AppLocalizations.of(context).disabled, + style: const TextStyle(fontSize: 13)), ), - ), - ], - ) - ) + ], + )) ], ); final Widget addReactionbutton = ColoredClickableCard( - color: Theme.of(context).colorScheme.secondaryContainer, - text: "Add a reaction", - onTap: () { - Reaction newreaction = Reaction.template(); - Navigator.of(context).pushNamed('/pipeline/action/new', - arguments: SetupActionPageArguments(newreaction) - ).then((r) { - if (newreaction != Reaction.template()) { - setState(() { - pipeline.reactions.add(newreaction); - }); - } - return r; - }); // TODO add reaction in db - } - ); + color: Theme.of(context).colorScheme.secondaryContainer, + text: AppLocalizations.of(context).addReaction, + onTap: () { + Reaction newreaction = Reaction.template(); + Navigator.of(context) + .pushNamed('/pipeline/action/new', + arguments: SetupActionPageArguments(newreaction)) + .then((r) { + if (newreaction != Reaction.template()) { + setState(() { + pipeline.reactions.add(newreaction); + }); + } + return r; + }); // TODO add reaction in db + }); final Widget deleteButton = ColoredClickableCard( color: Theme.of(context).colorScheme.error, - text: "Delete a Pipeline", + text: AppLocalizations.of(context).deletePipeline, onTap: () => showDialog( - context: context, - builder: (BuildContext context) => WarningDialog( - message: - "You are about to delete a pipeline. This action can not be undone. Are you sure ?", - onAccept: () { - provider.removePipeline(pipeline); - print("Delete pipeline"); /*TODO call api*/ - Navigator.of(context).pop(); - }, - warnedAction: "Delete" - ) - ), + context: context, + builder: (BuildContext context) => WarningDialog( + message: + AppLocalizations.of(context).deletePipelineWarningMessage, + onAccept: () { + provider.removePipeline(pipeline); + print("Delete pipeline"); /*TODO call api*/ + Navigator.of(context).pop(); + }, + warnedAction: AppLocalizations.of(context).delete)), ); return AerisCardPage( - body: Padding( - padding: const EdgeInsets.only(top: 10), - child: ListView( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 40), - child: cardHeader, - ), - const Text("Action", - style: TextStyle(fontWeight: FontWeight.w500) - ), - ActionCard( - leading: pipeline.trigger.service.getLogo(logoSize: 50), - title: pipeline.trigger.name, - trailing: ActionCardPopupMenu( + body: Padding( + padding: const EdgeInsets.only(top: 10), + child: ListView(children: [ + Padding( + padding: const EdgeInsets.only(bottom: 40), + child: cardHeader, + ), + const Text("Action", style: TextStyle(fontWeight: FontWeight.w500)), + ActionCard( + leading: pipeline.trigger.service.getLogo(logoSize: 50), + title: pipeline.trigger.name, + trailing: ActionCardPopupMenu( deletable: false, action: pipeline.trigger, - then: () => setState(() {})) - ), - const SizedBox(height: 25), - const Text("Reactions", - style: TextStyle(fontWeight: FontWeight.w500) - ), - for (var reaction in pipeline.reactions) - ActionCard( - leading: reaction.service.getLogo(logoSize: 50), - title: reaction.name, - trailing: ActionCardPopupMenu( - deletable: reaction != pipeline.reactions.first, - action: reaction, - then: () => setState(() {})) - ), - addReactionbutton, - const Padding( - padding: EdgeInsets.only(top: 30, bottom: 5), - child: Text("Danger Zone", - style: TextStyle(fontWeight: FontWeight.w500) - ) - ), - deleteButton, - const SizedBox(height: 25), - ] - ), - ) - ); - } - ); + then: () => setState(() {}))), + const SizedBox(height: 25), + const Text("Reactions", + style: TextStyle(fontWeight: FontWeight.w500)), + for (var reaction in pipeline.reactions) + ActionCard( + leading: reaction.service.getLogo(logoSize: 50), + title: reaction.name, + trailing: ActionCardPopupMenu( + deletable: reaction != pipeline.reactions.first, + action: reaction, + then: () => setState(() {}), + onDelete: () { + pipeline.reactions.remove(reaction); + }, + )), + addReactionbutton, + const Padding( + padding: EdgeInsets.only(top: 30, bottom: 5), + child: Text("Danger Zone", + style: TextStyle(fontWeight: FontWeight.w500))), + deleteButton, + const SizedBox(height: 25), + ]), + )); + }); } diff --git a/mobile/lib/src/views/service_page.dart b/mobile/lib/src/views/service_page.dart index 45d460d..c8c973e 100644 --- a/mobile/lib/src/views/service_page.dart +++ b/mobile/lib/src/views/service_page.dart @@ -1,34 +1,40 @@ import 'package:flutter/material.dart'; +import 'package:mobile/src/models/pipeline.dart'; +import 'package:mobile/src/models/reaction.dart'; import 'package:mobile/src/models/service.dart'; +import 'package:mobile/src/providers/pipelines_provider.dart'; import 'package:mobile/src/providers/user_services_provider.dart'; import 'package:mobile/src/widgets/action_card.dart'; import 'package:mobile/src/widgets/aeris_card_page.dart'; import 'package:mobile/src/widgets/warning_dialog.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; ///Page listing connected & available services class ServicePage extends StatelessWidget { const ServicePage({Key? key}) : super(key: key); - List getServiceGroup(String groupName, Icon trailingIcon, void Function() onTap, BuildContext context) { - UserServiceProvider uServicesProvider = Provider.of(context); + List getServiceGroup(String groupName, Icon trailingIcon, + void Function(Service) onTap, BuildContext context) { + UserServiceProvider uServicesProvider = + Provider.of(context); return [ - Text("$groupName:", + Text( + "$groupName:", style: const TextStyle(fontWeight: FontWeight.w500), ), const SizedBox(height: 10), for (var service in uServicesProvider.userServices) ActionCard( - leading: service.serviceProvider.getLogo(logoSize: 50), - title: service.serviceProvider.name, - trailing: IconButton( - splashColor: trailingIcon.color!.withAlpha(100), - splashRadius: 20, - icon: trailingIcon, - onPressed: onTap, - ) - ), + leading: service.serviceProvider.getLogo(logoSize: 50), + title: service.serviceProvider.name, + trailing: IconButton( + splashColor: trailingIcon.color!.withAlpha(100), + splashRadius: 20, + icon: trailingIcon, + onPressed: () => onTap(service.serviceProvider), + )), const SizedBox(height: 30), ]; } @@ -43,46 +49,66 @@ class ServicePage extends StatelessWidget { Service.twitter(), Service.spotify() ]; - UserServiceProvider uServiceProvider = Provider.of(context, listen: false); + UserServiceProvider uServiceProvider = + Provider.of(context, listen: false); for (var service in services) { uServiceProvider.createUserService(service); } - return AerisCardPage( - body: NotificationListener( - onNotification: (overscroll) { - overscroll.disallowIndicator(); - return true; - }, - child: ListView( - children: [ - ...[ - const Align( - alignment: Alignment.center, - child: Text("Services", - style: TextStyle(fontSize: 25) + return Consumer( + builder: (context, provider, _) => AerisCardPage( + body: NotificationListener( + onNotification: (overscroll) { + overscroll.disallowIndicator(); + return true; + }, + child: ListView( + children: [ + ...[ + const Align( + alignment: Alignment.center, + child: Text("Services", style: TextStyle(fontSize: 25)), ), - ), - const SizedBox(height: 60) + const SizedBox(height: 60) + ], + ...getServiceGroup( + AppLocalizations.of(context).connected, + const Icon(Icons.delete, color: Colors.red), + (Service service) => showDialog( + context: context, + builder: (BuildContext context) => WarningDialog( + message: AppLocalizations.of(context) + .disconnectServiceWarningMessage, + onAccept: () => { + provider.pipelineCollection.pipelines + .removeWhere((Pipeline pipeline) { + if (pipeline.trigger.service == service) { + return true; + } + if (pipeline.reactions + .where((Reaction react) => + react.service == service) + .isNotEmpty) { + return true; + } + return false; + }), + /// TODO Remove service from provider + provider.notifyListeners(), + print("Disconnect") + } /* TODO Delete service form db + related actions*/, + warnedAction: + AppLocalizations.of(context).disconnect)), + context), + ...getServiceGroup( + AppLocalizations.of(context).available, + const Icon(Icons.connect_without_contact, + color: Colors.green), + (Service service) => + print("Connected") /* TODO open page to connect service*/, + context), ], - ...getServiceGroup( - "Connected", - const Icon(Icons.delete, color: Colors.red), - () => showDialog( - context: context, - builder: (BuildContext context) => WarningDialog( - message: "You are about to disconnect to a service. Once disconnected, every related pipeline will be deleted. This action cannot be undone.", - onAccept: () => print("Disconnect") /* TODO Delete service form db + related actions*/, - warnedAction: "Disconnect") - ), - context - ), - ...getServiceGroup( - "Available", - const Icon(Icons.connect_without_contact, color: Colors.green), - () => print("Connected") /* TODO open page to connect service*/, - context), - ], + ), ), ), ); diff --git a/mobile/lib/src/views/setup_action_page.dart b/mobile/lib/src/views/setup_action_page.dart index eec5f66..65d2cd4 100644 --- a/mobile/lib/src/views/setup_action_page.dart +++ b/mobile/lib/src/views/setup_action_page.dart @@ -5,6 +5,7 @@ import 'package:mobile/src/models/trigger.dart'; import 'package:mobile/src/widgets/action_form.dart'; import 'package:mobile/src/widgets/aeris_card_page.dart'; import 'package:expandable/expandable.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; /// Class to get the action in route's arguments class SetupActionPageArguments { @@ -38,7 +39,7 @@ class _SetupActionPageState extends State { last: DateTime.now(), service: arguments.action.service, name: "action", - parameters: {'key1': 'value1', 'key2': null}) + parameters: {'key1': 'value1', 'key2': 'value2'}) ]; final Widget serviceDropdown = DropdownButton( @@ -82,7 +83,7 @@ class _SetupActionPageState extends State { Align( alignment: Alignment.centerLeft, child: Text( - "${availableActions.length} available actions for ", + "${availableActions.length} ${AppLocalizations.of(context).avalableActionsFor} ", )), Align(alignment: Alignment.centerRight, child: serviceDropdown), ], @@ -104,6 +105,7 @@ class _SetupActionPageState extends State { name: availableAction.name, parametersNames: availableAction.parameters.keys.toList(), + initValues: arguments.action.parameters, onValidate: (parameters) { arguments.action.service = serviceState!; arguments.action.parameters = parameters; diff --git a/mobile/lib/src/views/startup_page.dart b/mobile/lib/src/views/startup_page.dart index 65feea8..8507899 100644 --- a/mobile/lib/src/views/startup_page.dart +++ b/mobile/lib/src/views/startup_page.dart @@ -1,7 +1,7 @@ import 'package:flutter_fadein/flutter_fadein.dart'; import 'package:flutter/material.dart'; import '../widgets/aeris_page.dart'; - +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import '../../aeris.dart'; /// [StatefulWidget] used in order to display StartupPage [Widget] @@ -28,12 +28,12 @@ class _StartupPageState extends State { curve: Curves.easeInOut ) ), - const Padding( - padding: EdgeInsets.all(20), + Padding( + padding: const EdgeInsets.all(20), child: OverlayedText( - text: "Aeris is the best AREA in Nantes! Control each of your social network with Aeris, your new Action / Reaction app.", - overlayedColor: Color.fromRGBO(50, 0, 27, 1), - textColor: Color.fromRGBO(198, 93, 151, 1), + text: AppLocalizations.of(context).aerisDescription, + overlayedColor: const Color.fromRGBO(50, 0, 27, 1), + textColor: const Color.fromRGBO(198, 93, 151, 1), fontSize: 20, strokeWidth: 2.15 ) @@ -48,9 +48,9 @@ class _StartupPageState extends State { onPressed: () { Navigator.of(context).pushNamed('/login'); }, - child: const Tooltip( + child: Tooltip( message: 'Connexion', - child: Text("Se connecter") + child: Text(AppLocalizations.of(context).connect) ), ), ) diff --git a/mobile/lib/src/widgets/action_card_popup_menu.dart b/mobile/lib/src/widgets/action_card_popup_menu.dart index 7f1d867..e3a4eed 100644 --- a/mobile/lib/src/widgets/action_card_popup_menu.dart +++ b/mobile/lib/src/widgets/action_card_popup_menu.dart @@ -3,15 +3,21 @@ import 'package:mobile/src/views/setup_action_page.dart'; import 'package:mobile/src/widgets/aeris_popup_menu.dart'; import 'package:mobile/src/widgets/aeris_popup_menu_item.dart'; import 'package:mobile/src/models/action.dart' as aeris; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; /// [StatelessWidget] displayed as a PopupMenu class ActionCardPopupMenu extends StatelessWidget { - const ActionCardPopupMenu({ + ActionCardPopupMenu({ Key? key, required this.action, required this.then, required this.deletable, - }) : super(key: key); + this.onDelete, + }) : super(key: key) { + if (deletable) { + assert(onDelete != null); + } + } /// Action to trigger final aeris.Action action; @@ -22,36 +28,45 @@ class ActionCardPopupMenu extends StatelessWidget { /// Deletable characteristic final bool deletable; + final void Function()? onDelete; + @override Widget build(BuildContext context) { return AerisPopupMenu( - onSelected: (value) { - Map object = value as Map; - Navigator.pushNamed(context, object['route'] as String, arguments: object['params']).then((r) { - then(); - return r; - }); - }, - icon: Icons.more_vert, - itemBuilder: (context) => [ - AerisPopupMenuItem( - context: context, - icon: Icons.settings, - title: "Modify", - value: { - 'route': "/pipeline/action/mod", - 'params': SetupActionPageArguments(action), - }), - AerisPopupMenuItem( - context: context, - icon: Icons.delete, - title: "Delete", - value: "/pipeline/action/del", - enabled: deletable, - // TODO Delete from parent pipeline - /* TODO Define delete route*/ - ), - ] - ); + onSelected: (value) { + if (value == '/pipeline/action/del') { + onDelete!(); + ///TODO delete from db + } else { + Map object = value as Map; + Navigator.pushNamed(context, object['route'] as String, + arguments: object['params']) + .then((r) { + then(); + return r; + }); + } + ; + }, + icon: Icons.more_vert, + itemBuilder: (context) => [ + AerisPopupMenuItem( + context: context, + icon: Icons.settings, + title: AppLocalizations.of(context).modify, + value: { + 'route': "/pipeline/action/mod", + 'params': SetupActionPageArguments(action), + }), + AerisPopupMenuItem( + context: context, + icon: Icons.delete, + title: AppLocalizations.of(context).delete, + value: "/pipeline/action/del", + enabled: deletable, + // TODO Delete from parent pipeline + /* TODO Define delete route*/ + ), + ]); } } diff --git a/mobile/lib/src/widgets/action_form.dart b/mobile/lib/src/widgets/action_form.dart index dbbad41..5913fd7 100644 --- a/mobile/lib/src/widgets/action_form.dart +++ b/mobile/lib/src/widgets/action_form.dart @@ -1,3 +1,5 @@ +import 'dart:ffi'; + import 'package:flutter/material.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:form_builder_validators/form_builder_validators.dart'; @@ -9,7 +11,7 @@ class ActionForm extends StatefulWidget { /// Names of the parameters final List parametersNames; /// Initial values of the fields - final Map initValues; + final Map initValues; /// On validate callback final void Function(Map) onValidate; @@ -44,6 +46,7 @@ class _ActionFormState extends State { validator: FormBuilderValidators.compose([ FormBuilderValidators.required(context), ]), + keyboardType: (widget.initValues.containsKey(name)) && widget.initValues[name] is Int ? TextInputType.number : null, )), ...[ ElevatedButton( diff --git a/mobile/lib/src/widgets/aeris_page.dart b/mobile/lib/src/widgets/aeris_page.dart index 25613e0..cbc3005 100644 --- a/mobile/lib/src/widgets/aeris_page.dart +++ b/mobile/lib/src/widgets/aeris_page.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:mobile/src/widgets/background/animated_background.dart'; - +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; /// Application base page, holds scaffold and background class AerisPage extends StatelessWidget { /// Body of the page diff --git a/mobile/lib/src/widgets/home_page_menu.dart b/mobile/lib/src/widgets/home_page_menu.dart index 1f4f812..fc03632 100644 --- a/mobile/lib/src/widgets/home_page_menu.dart +++ b/mobile/lib/src/widgets/home_page_menu.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:mobile/src/widgets/aeris_popup_menu.dart'; import 'package:mobile/src/widgets/aeris_popup_menu_item.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; /// Menu for the Home Page class HomePageMenu extends StatelessWidget { @@ -11,10 +12,16 @@ class HomePageMenu extends StatelessWidget { return AerisPopupMenu( itemBuilder: (context) => [ AerisPopupMenuItem( - context: context, - icon: Icons.electrical_services, title: "Services", value: "/services"), + context: context, + icon: Icons.electrical_services, + title: AppLocalizations.of(context).services, + value: "/services"), AerisPopupMenuItem( - context: context, icon: Icons.logout, title: "Logout", value: "/logout"), + + context: context, + icon: Icons.logout, + title: AppLocalizations.of(context).logout, + value: "/logout"), ], onSelected: (route) => Navigator.pushNamed(context, route as String), icon: Icons.more_horiz, diff --git a/mobile/lib/src/widgets/home_page_sort_menu.dart b/mobile/lib/src/widgets/home_page_sort_menu.dart index dfb2573..cdc2a17 100644 --- a/mobile/lib/src/widgets/home_page_sort_menu.dart +++ b/mobile/lib/src/widgets/home_page_sort_menu.dart @@ -4,6 +4,7 @@ import 'package:mobile/src/providers/pipelines_provider.dart'; import 'package:mobile/src/widgets/aeris_popup_menu.dart'; import 'package:mobile/src/widgets/aeris_popup_menu_item.dart'; import 'package:recase/recase.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; /// Sorting Menu for the Home Page class HomePageSortMenu extends StatelessWidget { @@ -33,27 +34,27 @@ class HomePageSortMenu extends StatelessWidget { ...[ for (var sortingMethod in PipelineCollectionSort.values) AerisPopupMenuItem( - context: context, - icon: sortMethodGetIcon(sortingMethod), - title: ReCase(sortingMethod.name).titleCase, - value: sortingMethod - ), + context: context, + icon: sortMethodGetIcon(sortingMethod), + title: ReCase(sortingMethod.name).titleCase, + value: sortingMethod), ], AerisPopupMenuItem( - context: context, - icon: Icons.call_merge, - title: collectionProvider.pipelineCollection.sortingSplitDisabled - ? "Merge disabled pipelines" - : "Seperate disabled pipelines", - value: "" - ), + context: context, + icon: Icons.call_merge, + title: collectionProvider.pipelineCollection.sortingSplitDisabled + ? AppLocalizations.of(context).mergeDisabledPipelines + : AppLocalizations.of(context).seperateDisabledPipelines, + value: ""), ], onSelected: (sortingMethod) { /// TODO: not clean if (sortingMethod == "") { - collectionProvider.pipelineCollection.sortingSplitDisabled = !collectionProvider.pipelineCollection.sortingSplitDisabled; + collectionProvider.pipelineCollection.sortingSplitDisabled = + !collectionProvider.pipelineCollection.sortingSplitDisabled; } else { - collectionProvider.pipelineCollection.sortingMethod = sortingMethod as PipelineCollectionSort; + collectionProvider.pipelineCollection.sortingMethod = + sortingMethod as PipelineCollectionSort; } collectionProvider.sortPipelines(); }, diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 4560ef7..44edf20 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -571,7 +571,7 @@ packages: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.8" url_launcher_windows: dependency: transitive description: @@ -592,21 +592,21 @@ packages: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.1.1" + version: "2.1.2" win32: dependency: transitive description: name: win32 url: "https://pub.dartlang.org" source: hosted - version: "2.3.9" + version: "2.3.11" xdg_directories: dependency: transitive description: name: xdg_directories url: "https://pub.dartlang.org" source: hosted - version: "0.2.0" + version: "0.2.0+1" xml: dependency: transitive description: @@ -623,4 +623,4 @@ packages: version: "3.1.0" sdks: dart: ">=2.16.0-100.0.dev <3.0.0" - flutter: ">=2.5.0" + flutter: ">=2.10.0" diff --git a/mobile/test/widget_test.dart b/mobile/test/widget_test.dart index 51b48fe..514db74 100644 --- a/mobile/test/widget_test.dart +++ b/mobile/test/widget_test.dart @@ -12,7 +12,7 @@ import 'package:mobile/src/main.dart'; void main() { testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. - await tester.pumpWidget(const MyApp()); + await tester.pumpWidget(const Aeris()); // Verify that our counter starts at 0. expect(find.text('0'), findsOneWidget);