Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • private-contact-logging/interface-utilisateur
1 result
Show changes
Commits on Source (6)
assets/triple_dots.png

10.2 KiB

assets/triple_dots_blue.png

10.4 KiB

assets/triple_dots_red.png

10.4 KiB

...@@ -125,39 +125,11 @@ class QR extends StatelessWidget { ...@@ -125,39 +125,11 @@ class QR extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Column( return QrImage(
mainAxisAlignment: MainAxisAlignment.spaceAround, data: jsonEncode(_codeWrapperQR()),
children: [ version: QrVersions.auto,
Container( size: MediaQuery.of(context).size.height * 0.25,
alignment: Alignment.center, foregroundColor: Theme.of(context).primaryColorLight,
width: MediaQuery.of(context).size.width,
height: MediaQuery.of(context).size.height * 0.60,
child: QrImage(
data: jsonEncode(_codeWrapperQR()),
version: QrVersions.auto,
size: MediaQuery.of(context).size.width * 0.48,
foregroundColor: Theme.of(context).primaryColorLight,
),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("./assets/VirusSansQR.png"),
fit: BoxFit.fitWidth)),
),
Container(
margin: EdgeInsets.only(
top: 20,
bottom: 40,
),
child: Text(
this.name,
style: TextStyle(
fontSize: 48,
color: Theme.of(context).primaryColor,
),
textAlign: TextAlign.center,
),
),
],
); );
} }
} }
import 'package:flutter/material.dart';
import 'package:naca/models/Event.dart';
import 'package:naca/primaryButton.dart';
import 'package:naca/secondaryButton.dart';
import 'utils.dart';
import 'QR.dart';
/// Wrapper du QR-code d'un événement. Il permet d'afficher son nom et son QR, mais il permet
/// également de signaler l'événement passé en paramètre.
class QRDeco extends StatefulWidget {
final Event event;
const QRDeco({
@required this.event,
});
@override
_QRDecoState createState() => _QRDecoState();
}
class _QRDecoState extends State<QRDeco> with TickerProviderStateMixin {
bool initInfected;
// Variable indiquant l'écran à afficher
Screen currentScreen = Screen.qr;
AnimationController _controller;
Animation<Color> animation;
@override
void initState() {
super.initState();
// Définition du contrôleur qui sera utilisé pour le changement de couleur
_controller = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
// Initialisation de la variable indiquant si l'événement était déjà infecté à la création du Widget
initInfected = widget.event.infected;
}
@override
dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
// Définition de l'animation de transition de couleurs, du thème au rouge
animation = ColorTween(
begin: Theme.of(context).primaryColor,
end: Colors.red,
).animate(_controller)
..addListener(() {
setState(() {});
});
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// Espace vide prenant 10% de l'espace
Spacer(
flex: 1,
),
// QR et titre de l'event prenant 80% de l'espace
Expanded(
flex: 8,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Padding(
padding: EdgeInsets.only(top: 40),
// Permet de faire la transition entre le QR et les options de signalement
child: AnimatedSwitcher(
// On a besoin de redéfinir le layout, car c'est un Stack par défaut et la hauteur des éléments n'est plus adaptée
layoutBuilder:
(Widget currentChild, List<Widget> previousChildren) {
return currentChild;
},
// Transition jouant avec l'opacité et la taille des éléments
transitionBuilder:
(Widget child, Animation<double> animation) {
return FadeTransition(
opacity: animation,
child: ScaleTransition(
scale: animation,
child: child,
),
);
},
duration: Duration(milliseconds: 300),
child: _getScreen(),
),
),
),
// Titre de l'événement
Container(
margin: EdgeInsets.only(
top: 20,
bottom: 70,
),
child: Text(
_getShortenedText(widget.event.name),
style: TextStyle(
fontSize: _getSizeFromText(widget.event.name),
color: Theme.of(context).primaryColor,
),
textAlign: TextAlign.center,
),
),
],
),
),
// Espace prenant 10% de l'espace avec le bouton pour accéder aux options de signalement
Expanded(
flex: 1,
child: Column(
children: [
// Bouton
Padding(
padding: EdgeInsets.only(
top: MediaQuery.of(context).size.height * 0.05),
child: FlatButton(
onPressed: () {
setState(() {
// Si l'écran courant est le QR, alors on peut accéder aux écrans de signalement
if (currentScreen == Screen.qr) {
currentScreen = widget.event.infected
? Screen.eventSignaled
: Screen.signalButton;
// Sinon on affiche le QR
} else
currentScreen = Screen.qr;
});
},
child: Image.asset(
widget.event.infected
? "./assets/triple_dots_red.png"
: "./assets/triple_dots_blue.png",
scale: 10,
),
),
),
// Element prenant le reste de l'espace pour mettre le bouton tout en haut de la colonne
Spacer(),
],
),
),
],
);
}
/// Méthode retournant le widget de l'écran actuel
Widget _getScreen() {
switch (currentScreen) {
case Screen.qr:
return _buildQRScreen();
break;
case Screen.signalButton:
return _buildSignalButtonScreen();
break;
case Screen.confirmation:
return _buildConfirmationScreen();
break;
case Screen.loading:
return _buildLoadingScreen();
break;
case Screen.error:
return _buildErrorScreen();
break;
case Screen.eventSignaled:
return _buildEventSignaledScreen();
break;
default:
return _buildQRScreen();
}
}
/// Méthode construisant le QR
Widget _buildQRScreen() {
return Container(
child: Center(child: QR.fromEvent(widget.event)),
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("./assets/VirusSansQR.png"),
fit: BoxFit.contain,
),
),
);
}
/// Méthode construisant le bouton pour signaler l'événement
Widget _buildSignalButtonScreen() {
return PrimaryButton(
body: "Signaler\ncet événement",
fontSize: 35,
onPressed: () {
setState(() {
currentScreen = Screen.confirmation;
});
},
);
}
/// Méthode construisant l'écran de confirmation de signalement
Widget _buildConfirmationScreen() {
return PrimaryButton(
child: Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Text(
"Souhaitez-vous vraiment signaler un cas de covid-19 à cet événement ?",
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).primaryColorLight,
fontSize: 20,
),
),
Container(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_customSecondaryButton(
body: "Oui",
widthPercentage: 0.35,
onPressed: _signalEvent,
),
_customSecondaryButton(
body: "Non",
widthPercentage: 0.35,
onPressed: () {
setState(() {
currentScreen = Screen.qr;
});
},
),
],
),
),
Text(
"Cette action est irréversible",
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).primaryColorLight,
fontSize: 17,
),
),
],
),
),
);
}
/// Méthode construisant l'écran de chargement
Widget _buildLoadingScreen() {
return PrimaryButton(
// On utilise la valeur de transition d'animation.
// Si l'envoi au backend marche, la couleur vire sur le rouge
backgroundColor: animation.value,
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
"Signalement de l'événement en cours...",
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).primaryColorLight,
fontSize: 20,
),
),
// Widget affichant une roue circulaire de chargement classique
CircularProgressIndicator(
valueColor:
AlwaysStoppedAnimation(Theme.of(context).primaryColorLight),
),
],
),
);
}
/// Méthode construisant l'écran d'erreur
Widget _buildErrorScreen() {
return PrimaryButton(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
"Une erreur est survenue. Voulez-vous réessayer?",
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).primaryColorLight,
fontSize: 20,
),
),
_customSecondaryButton(
body: "Réessayer",
onPressed: _signalEvent,
widthPercentage: 0.5,
),
],
),
);
}
/// Méthode construisant l'écran à afficher lorsque l'événement a été signalé
Widget _buildEventSignaledScreen() {
return PrimaryButton(
body: "Cet événement a été signalé comme contaminé.",
// On doit checker avec initInfected pour le cas où l'animation n'a jamais été lancée
// et la couleur reste sur celle du thème
backgroundColor: initInfected ? Colors.red : animation.value,
fontSize: 30,
onPressed: () {
setState(() {
currentScreen = Screen.qr;
});
},
);
}
/// Méthode construisant un preset de bouton réutilisable
Widget _customSecondaryButton(
{String body, Function onPressed, double widthPercentage}) {
return Container(
width: MediaQuery.of(context).size.width * widthPercentage,
margin: EdgeInsets.symmetric(vertical: 10),
child: SecondaryButton(
body: body,
onPressed: onPressed,
),
);
}
/// Méthode signalant l'événement
void _signalEvent() {
setState(() {
// On commence par afficher l'écran de chargement
currentScreen = Screen.loading;
});
// Petit délai pour que les transitions ne soient pas trop brusques
Future.delayed(const Duration(seconds: 2), () {
// Signalement de l'événement
sendCheckCode(widget.event, context)
// Timeout pour mettre une limite à l'attente
.timeout(Duration(seconds: 15))
.then((success) {
//Si réussite
setState(() {
// On passe de l'écran de chargement à l'écran informant que l'événement a bien été signalé
currentScreen = Screen.eventSignaled;
// Activation de l'animation de transition de la couleur vers le rouge
_controller.forward();
// Mise à jour de l'attribut de l'événement
widget.event.infected = true;
widget.event.save();
});
}).catchError((e) {
// S'il y a eu une erreur, comme un timeout par exemple
print(e);
setState(() {
currentScreen = Screen.error;
});
});
});
}
}
/// Méthode retournant une taille à utiliser pour fontSize selon le nombre de caractères
double _getSizeFromText(String text) {
if (text.length > 15) {
return 30;
} else if (text.length > 10) {
return 40;
} else {
return 50;
}
}
/// Méthode retournant une chaîne de caractères plus courte si nécessaire
String _getShortenedText(String text) {
if (text.length > 15)
return text.substring(0, 15) + "...";
else
return text;
}
/// Enumération comprenant tous les écrans affichables pour un événement (QR et signalement)
enum Screen {
qr,
signalButton,
confirmation,
loading,
error,
eventSignaled,
}
...@@ -27,6 +27,7 @@ class _CreateEventState extends State<CreateEvent> { ...@@ -27,6 +27,7 @@ class _CreateEventState extends State<CreateEvent> {
context: controllers['Contexte'], context: controllers['Contexte'],
startDate: controllers['Date de début'], startDate: controllers['Date de début'],
endDate: controllers['Date de fin'], endDate: controllers['Date de fin'],
infected: false,
created: true, created: true,
); );
eventCreated.add(event); eventCreated.add(event);
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:naca/QRDeco.dart';
import 'package:naca/primaryButton.dart'; import 'package:naca/primaryButton.dart';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart'; import 'package:hive_flutter/hive_flutter.dart';
import 'package:naca/boutonScan.dart'; import 'package:naca/boutonScan.dart';
import 'package:naca/carouselQR.dart'; import 'package:naca/carouselQR.dart';
import 'package:naca/createEvent.dart'; import 'package:naca/createEvent.dart';
import 'package:naca/QR.dart';
import 'package:naca/models/Event.dart'; import 'package:naca/models/Event.dart';
import 'package:naca/personalQR.dart'; import 'package:naca/personalQR.dart';
...@@ -16,6 +16,7 @@ class HomePage extends StatefulWidget { ...@@ -16,6 +16,7 @@ class HomePage extends StatefulWidget {
class _HomePageState extends State<HomePage> { class _HomePageState extends State<HomePage> {
Box<Event> hiveBox; Box<Event> hiveBox;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
...@@ -40,7 +41,9 @@ class _HomePageState extends State<HomePage> { ...@@ -40,7 +41,9 @@ class _HomePageState extends State<HomePage> {
pages.add(PersonalQR()); pages.add(PersonalQR());
pages.addAll(events pages.addAll(events
.where((element) => element.created == true) .where((element) => element.created == true)
.map((e) => QR.fromEvent(e))); .map((e) {
return QRDeco(event: e);
}));
pages.add(CreateEvent()); pages.add(CreateEvent());
return CarouselQR(pages); return CarouselQR(pages);
}, },
......
...@@ -5,15 +5,20 @@ class PrimaryButton extends StatelessWidget { ...@@ -5,15 +5,20 @@ class PrimaryButton extends StatelessWidget {
/// Texte à afficher /// Texte à afficher
final String body; final String body;
final Widget child;
/// Fonction à utiliser lors d'un click /// Fonction à utiliser lors d'un click
final Function onPressed; final Function onPressed;
/// Taille de la police /// Taille de la police
final double fontSize; final double fontSize;
final Color backgroundColor;
final EdgeInsets padding; final EdgeInsets padding;
PrimaryButton({ PrimaryButton({
@required this.body, this.body,
this.child,
this.onPressed, this.onPressed,
this.fontSize, this.fontSize,
this.padding = const EdgeInsets.only( this.padding = const EdgeInsets.only(
...@@ -22,26 +27,31 @@ class PrimaryButton extends StatelessWidget { ...@@ -22,26 +27,31 @@ class PrimaryButton extends StatelessWidget {
left: 40, left: 40,
right: 40, right: 40,
), ),
this.backgroundColor,
}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return Container(
child: OutlinedButton( child: OutlinedButton(
child: Text( child: body == null
body, ? child
style: TextStyle( : Text(
color: Theme.of(context).primaryColorLight, body,
fontSize: fontSize, textAlign: TextAlign.center,
), style: TextStyle(
), color: Theme.of(context).primaryColorLight,
fontSize: fontSize,
),
),
onPressed: onPressed, onPressed: onPressed,
style: ButtonStyle( style: ButtonStyle(
padding: MaterialStateProperty.all<EdgeInsets>( padding: MaterialStateProperty.all<EdgeInsets>(
padding, padding,
), ),
backgroundColor: backgroundColor: backgroundColor == null
MaterialStateProperty.all<Color>(Theme.of(context).primaryColor), ? MaterialStateProperty.all<Color>(Theme.of(context).primaryColor)
: MaterialStateProperty.all<Color>(backgroundColor),
shape: MaterialStateProperty.all<RoundedRectangleBorder>( shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder( RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0), borderRadius: BorderRadius.circular(8.0),
......
import 'package:flutter/material.dart';
class SecondaryButton extends StatelessWidget {
final String body;
final Function onPressed;
final double fontSize;
SecondaryButton({this.body, this.onPressed, this.fontSize = 30});
@override
Widget build(BuildContext context) {
return OutlinedButton(
onPressed: onPressed,
child: Text(
body,
textAlign: TextAlign.center,
style: TextStyle(
color: Theme.of(context).primaryColorLight,
fontSize: fontSize,
),
),
style: ButtonStyle(
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
),
padding: MaterialStateProperty.all(
EdgeInsets.symmetric(vertical: 6, horizontal: 20)),
side: MaterialStateProperty.all<BorderSide>(
BorderSide(
width: 4,
color: Theme.of(context).primaryColorLight,
),
),
),
);
}
}
import 'dart:convert'; import 'dart:convert';
import 'package:hive/hive.dart'; import 'package:hive/hive.dart';
import 'package:toast/toast.dart';
import 'models/Event.dart'; import 'models/Event.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
...@@ -71,7 +70,7 @@ Future<List> fetchCheckCodes() async { ...@@ -71,7 +70,7 @@ Future<List> fetchCheckCodes() async {
} }
// POST - checkCode // POST - checkCode
Future<void> sendCheckCode(Event event, context) async { Future<bool> sendCheckCode(Event event, context) async {
var message = { var message = {
'secretKey': event.checkCode, 'secretKey': event.checkCode,
'startTime': event.startDate.millisecondsSinceEpoch.toString(), 'startTime': event.startDate.millisecondsSinceEpoch.toString(),
...@@ -91,13 +90,10 @@ Future<void> sendCheckCode(Event event, context) async { ...@@ -91,13 +90,10 @@ Future<void> sendCheckCode(Event event, context) async {
}, },
body: jsonEncode(message), body: jsonEncode(message),
); );
if (response.statusCode == 200) { if (response.statusCode == 200) {
//TODO: Do what u want here when sucessfully sent. return true;
Toast.show('Event ${event.name} signaled as infected', context,
duration: 5);
} else { } else {
// print(message);
throw Exception(response.body); throw Exception(response.body);
} }
} }
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:naca/QRDeco.dart';
import 'package:hive/hive.dart';
import 'package:hive_flutter/hive_flutter.dart';
import 'package:naca/models/Event.dart';
import 'package:naca/primaryButton.dart';
import 'package:naca/secondaryButton.dart';
import 'package:qr_flutter/qr_flutter.dart';
void main() {
Box<Event> eventDB;
setUpAll(() async {
// We init the Hive
await Hive.initFlutter();
// Since our classes are not native elements, we generate Adapters
Hive.registerAdapter<Event>(EventAdapter());
// We open the boxes, we can open them anywhere.
// It is better to close them after using it.
eventDB = await Hive.openBox<Event>('event');
TestWidgetsFlutterBinding.ensureInitialized();
});
testWidgets('QR, text and all the screens to signal an event are displayed',
(WidgetTester tester) async {
Event event = new Event(
name: 'Rendez-vous',
location: 'la-bas',
context: 'RDV',
startDate: DateTime(2021, 16, 02),
infected: false,
);
eventDB.add(event);
final qrDeco = makeTesteableWidget(
child: QRDeco(event: event),
);
await tester.pumpWidget(qrDeco);
// Affichage initial: QR et nom d'événement
expect(find.byType(QrImage), findsOneWidget);
expect(find.text(event.name), findsOneWidget);
// On appuie sur le bouton pour afficher le bouton de signalement
await tester.tap(find.byType(FlatButton));
// On a besoin du settle vu qu'il y a une transition entre les états
await tester.pumpAndSettle();
expect(find.text("Signaler\ncet événement"), findsOneWidget);
expect(find.byType(PrimaryButton), findsOneWidget);
// On navigue jusqu'à l'écran de confirmation
await tester.tap(find.byType(PrimaryButton));
await tester.pump();
expect(find.byType(PrimaryButton), findsOneWidget);
expect(
find.text(
"Souhaitez-vous vraiment signaler un cas de covid-19 à cet événement ?"),
findsOneWidget);
expect(find.byType(SecondaryButton), findsNWidgets(2));
expect(find.text("Cette action est irréversible"), findsOneWidget);
// Retour à l'écran initial
await tester.tap(find.text("Non"));
await tester.pumpAndSettle();
expect(find.byType(QrImage), findsOneWidget);
expect(find.text(event.name), findsOneWidget);
// TODO vérifier le click sur "Oui" et attendre la réponse du backend avant de tester
});
}
Widget makeTesteableWidget({Widget child}) {
return MaterialApp(
home: Scaffold(
body: child,
),
);
}