Mobile client: Translation System

Mobile client: Translation System
This commit is contained in:
Arthur Jamet
2022-02-09 10:32:31 +01:00
committed by GitHub
23 changed files with 517 additions and 397 deletions
+2
View File
@@ -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
+1 -1
View File
@@ -1,3 +1,3 @@
arb-dir: lib/l10n
template-arb-file: app_en.arb
output-localization-file: app_localizations.dartà
output-localization-file: app_localizations.dart
-6
View File
@@ -1,6 +0,0 @@
{
"helloWorld": "Hello World!",
"@helloWorld": {
"description": "The conventional newborn programmer greeting"
}
}
-3
View File
@@ -1,3 +0,0 @@
{
"helloWorld": "Bonjour, monde!",
}
+29
View File
@@ -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."
}
+29
View File
@@ -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!"
}
+32 -30
View File
@@ -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<NavigatorState> materialKey = GlobalKey<NavigatorState>();
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,
));
});
}
}
+1 -1
View File
@@ -10,7 +10,7 @@ abstract class Action {
String name;
///Action's parameters
Map<String, Object?> parameters;
Map<String, Object> parameters;
Action(
{Key? key,
required this.service,
+1 -1
View File
@@ -8,7 +8,7 @@ class Reaction extends aeris_action.Action {
{Key? key,
required Service service,
required String name,
Map<String, Object?> parameters = const {}})
Map<String, Object> parameters = const {}})
: super(service: service, name: name, parameters: parameters);
/// Template trigger, used as an 'empty' trigger
+9 -4
View File
@@ -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<String, Object?> parameters = const {},
Map<String, Object> 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
+121 -108
View File
@@ -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<CreatePipelinePage> {
/// Creates a basic Template the user can modify
Trigger trigger = Trigger.template();
@@ -36,113 +36,126 @@ class _CreatePipelinePageState extends State<CreatePipelinePage> {
Widget build(BuildContext context) {
final _formKey = GlobalKey<FormBuilderState>();
return Consumer<PipelineProvider>(
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)
);
}
}
},
),
]),
)),
])
)
);
)),
])));
}
}
+22 -21
View File
@@ -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");
}));
}
}
+105 -111
View File
@@ -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<PipelineDetailPage> {
@override
Widget build(BuildContext context) =>
Consumer<PipelineProvider>(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<PipelineDetailPage> {
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<String>(
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),
]),
));
});
}
+72 -46
View File
@@ -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<Widget> getServiceGroup(String groupName, Icon trailingIcon, void Function() onTap, BuildContext context) {
UserServiceProvider uServicesProvider = Provider.of<UserServiceProvider>(context);
List<Widget> getServiceGroup(String groupName, Icon trailingIcon,
void Function(Service) onTap, BuildContext context) {
UserServiceProvider uServicesProvider =
Provider.of<UserServiceProvider>(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<UserServiceProvider>(context, listen: false);
UserServiceProvider uServiceProvider =
Provider.of<UserServiceProvider>(context, listen: false);
for (var service in services) {
uServiceProvider.createUserService(service);
}
return AerisCardPage(
body: NotificationListener<OverscrollIndicatorNotification>(
onNotification: (overscroll) {
overscroll.disallowIndicator();
return true;
},
child: ListView(
children: [
...[
const Align(
alignment: Alignment.center,
child: Text("Services",
style: TextStyle(fontSize: 25)
return Consumer<PipelineProvider>(
builder: (context, provider, _) => AerisCardPage(
body: NotificationListener<OverscrollIndicatorNotification>(
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),
],
),
),
),
);
+4 -2
View File
@@ -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<SetupActionPage> {
last: DateTime.now(),
service: arguments.action.service,
name: "action",
parameters: {'key1': 'value1', 'key2': null})
parameters: {'key1': 'value1', 'key2': 'value2'})
];
final Widget serviceDropdown = DropdownButton<Service>(
@@ -82,7 +83,7 @@ class _SetupActionPageState extends State<SetupActionPage> {
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<SetupActionPage> {
name: availableAction.name,
parametersNames:
availableAction.parameters.keys.toList(),
initValues: arguments.action.parameters,
onValidate: (parameters) {
arguments.action.service = serviceState!;
arguments.action.parameters = parameters;
+8 -8
View File
@@ -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<StartupPage> {
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<StartupPage> {
onPressed: () {
Navigator.of(context).pushNamed('/login');
},
child: const Tooltip(
child: Tooltip(
message: 'Connexion',
child: Text("Se connecter")
child: Text(AppLocalizations.of(context).connect)
),
),
)
@@ -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*/
),
]);
}
}
+4 -1
View File
@@ -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<String> parametersNames;
/// Initial values of the fields
final Map<String, String> initValues;
final Map<String, Object> initValues;
/// On validate callback
final void Function(Map<String, String>) onValidate;
@@ -44,6 +46,7 @@ class _ActionFormState extends State<ActionForm> {
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(context),
]),
keyboardType: (widget.initValues.containsKey(name)) && widget.initValues[name] is Int ? TextInputType.number : null,
)),
...[
ElevatedButton(
+1 -1
View File
@@ -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
+10 -3
View File
@@ -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,
+15 -14
View File
@@ -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();
},
+5 -5
View File
@@ -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"
+1 -1
View File
@@ -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);