Footer fixé en bas ultra simple avec CSS3

Bonjour bonjour !

J’en suis sûr, toi aussi tu t’es pris la tête sur ce foutu footer qui doit rester en bas même si le contenu n’est pas assez important ! Tu as mis du JS si tu étais un peu bourrin, ou tu as joué avec la technique du positon absolute et margin-top négatif, mais c’était lourd si ton footer changeait de taille, et puis c’était pas responsive…

Réjouis toi camarade, CSS3 pense à toi avec FLEX !

Flex, comment ça marche ? Et bien encore plus simplement que les tableaux.

D’abords prenons un squelette html5 simple :

<html>
  <body>
    <header>
      <h1>Mysterty propose un super site</h1>
    </header>
    <main>
      <p>
        voilà notre super site, merci Mysterty
      </p>
    </main>
    <footer>
      <p>
        et le footer fixé en bas
      </p>
      <p>
        fourni par <a href="https://www.mysterty.com/" title="mysterty, développeur web">notre partenaire</a>
      </p>
    </footer>
  </body>
</html>

Un header, un main, un footer, le tout dans un body, c’est tout ce dont on a besoin. Le reste c’est du CSS. L’idée va simplement être de définir le body comme une boite « FLEX », c’est à dire que son contenu sera adaptable, et de définir sa direction en « colonne » car on veut mettre le footer en bas (et on oublie pas de définir sa taille à 100% du support 😉 ).

body {
	display: flex;
	flex-direction: column;
	min-height: 100vh;
}

On a fait 90% du job en 3 directives ! On a plus qu’à caler le footer en bas. Pour celà, il suffit de définir que sa marge « haute » doit être de la totalité de l’espace disponible (si espace disponible il y a) :

footer {
	margin-top: auto;
}

Et voilà, vous avez un footer parfaitement adaptable, complètement responsive ready !

Petit plus si vous utilisez Bootstrap 4, tout est déjà prêt, vous n’avez que 2 chose à faire : définir la taille du body et le margin-auto du footer 😉

Enjoy !

Engagement politique, social et écologique

Je n’ai jamais su fermer les yeux. Ça m’a parfois valu des problèmes mais c’est comme ça.

En mars 2016, j’ai vu apparaître le mouvement Nuit Debout. J’ai vu des personnes de tout âge et de toute provenance s’asseoir en cercle et discuter ensemble. J’ai vu des personnes témoigner, d’autres informer, d’autres écouter… Et ça a été une libération. Je n’étais pas seul. Nous ne sommes pas seuls.

Dans toute la France nous nous sommes réunis. Notre mouvement est venu d’un petit caillou jeté dans l’eau vers un immense raz de marrée qui a commencé à submerger les esprits et à éroder les consciences. Et j’ai compris qu’on avait chacun quelque chose à apporter. Depuis La Rochelle, j’ai vu le mouvement évoluer, monter en puissance puis s’épuiser. Mais il a été la source qui a nourri de nombreuses graines dans notre société.

Pour ma part, j’ai commencé par créer une chaîne youtube dédiée à mes engagements politiques, sociaux et écologiques en postant une première vidéo sur la fermeture de l’usine Alstom de belfort, puis que j’ai continué à nourrir au fur et à mesure (n’hésitez pas à laisser des pouces bleus et à les partager d’ailleurs 😉 ).

Vidéo sur le mythe des chômeurs assistés et fraudeurs

Et je ne me suis pas arrêté là en lançant le site Les Devinettes de Mysterty dont le principe et d’inciter chacun à réagir sur des sujets d’actualité en mettant à contribution mes quelques notions de dessin (encore une fois, n’hésitez pas à partager 😉 ).Bref, si j’écris cet article, c’est pour partager avec vous cette expérience et vous rappeler que chacun peut être utile. C’est pour vous rappeler que des solutions existent et qu’elles sont à notre portée, à condition qu’on sorte de la terre gelée qu’on essaye de nous imposer. Renseignez-vous autour de vous sur les initiatives qui essayent de germer ou qui ont déjà des racines solides et nous promettent des fruits pleins d’espoir.

De nombreuses luttes éclatent partout en France. Nous pouvons tous changer cette société.

Refonte perso

Important changement d’image pour moi !

Après presque 10 ans de bons et loyaux services, j’offre sa retraite à mon premier site personnel en Flash.

version flash

Site perso version Flash


J’aurai passé beaucoup de temps à jouer avec les fonctionnalités de Flash, à découvrir Sandy3D et à améliorer l’expérience avec JQuery 1.2.

Mais Flash a passé de mode et JQuery 1.2 n’est plus maintenu, place aux jeunes…

Site perso

Preview du site mysterty.com


Et pour cette nouvelle version, on met google material à l’honneur et on s’attaque aux canvas.

À suivre, l’insertion de la 3D avec WebGL mais en attendant, n’hésitez pas à me contacter si vous avez un projet à réaliser ! 😉

Transition CSS basé sur la taille ajustable… possible ?

La réponse est oui.

Allez, vous aussi vous avez cherché à faire une jolie transition CSS sur votre sous-menu, non ? mais vous vous êtes demandés comment faire, car « height: auto » ne fonctionne pas avec les transition. Alors quoi ? on va quand même pas mettre une hauteur fixe… Et encore moins le faire en JS…

Et bien la solution est vraiment simple en fait. Ce n’est pas sur height qu’il faut se baser… mais sur max-height !

Le code tout de suite :

.sous-menu {
	display: block;
	overflow: hidden;
	max-height: 0;
	transition: max-height 2s;
}
.menu li:hover .sous-menu {
	max-height: 100em; // ça vous donne 100 ligne d'avance ;)
}

testez !

enjoy 😉

JeuxVidéos-Impact.com, un autre regard !

Plus d’un an que je n’avais plus posté sur ce blog. Et pourtant il s’en est passé des choses.

J’ai continué d’avancer et d’améliorer www.wantedlove.fr, j’ai fait de belles rencontres et j’ai également rejoint 2 amis pour monter les événements Drink and Draw La Rochelle.

Mais ce n’est pas le sujet de ce post. Aujourd’hui je vous présente www.jeuxvideos-impact.com ! Un site présentant un autre regard sur le monde des jeux vidéo et des nouveaux médias.

J’avais créé ce site il y a maintenant 5 ans comme projet pour mon Master 2. Malheureusement je ne l’avais jamais achevé et il était grand temps de lui offrir un nouvel habillage. Voilà qui est fait. Un design 100% responsive adapté aux appareils mobiles, une sécurité renforcée, de nouveaux articles et de nouvelles fonctionnalités, le tout au service des joueurs, des parents, des sociologues et des acteurs du jeu-vidéo.

jeuxvideos-impact, site sur l'impact des nouveaux médias

Il reste encore du travail bien sûr. La mise en place d’un forum, d’un chat et bien sûr des tests qui reviendront sous une forme nouvelle, mais surtout la rédaction d’un contenu de qualité !

Alors à très bientôt sur JeuxVidéos-Impact.com !

wantedlove

J’ai eu l’idée il y a 2 ans de faire un site de rencontre entièrement gratuit basé sur les affinités.

Le but était que chaque personne ait accès aux mêmes fonctionnalités sans avoir besoin de payer, car j’estime que toute interaction sociale ou amoureuse devrait être exemptée de taxe. De plus j’avais à cœur que chacun puisse trouver ce qu’il cherchait vraiment par un système de mot clef.

C’est désormais chose faite : wantedlove a enfin vu le jour et est ouvert à tous.

wantedlove : l'amour n'a pas de prime

Téléversez vos photos, complétez votre profil, et vous serez mis en relation avec les personnes qui aiment -ou n’aiment pas ;)- les mêmes choses que vous.
Devenez un chasseur ou une chasseuse de prime : plus l’affinité est élevée, plus la prime sera grande !

Ne payez plus pour rencontrer l’amour, rendez-vous sur wantedlove

_____
référencé sur : référencement gratuit

Nettoyer rapidement une image

Bonjour !

Depuis quelques temps, je poste mes illustrations sur le site Drawin. Et à de nombreuses reprises, on trouve malheureusement des personnes qui n’ont pas l’habitude de l’outil informatique pour partager leurs créations.

Je me décide donc à faire ce petit tuto à partir d’une image de Hupsina, photographiée dans le mauvais sens et non-nettoyée.

Nous utiliserons le logiciel Gimp, logiciel libre, gratuit, et puissant dédié à la retouche d’image.

L’image de base

Voici donc notre base de travail.

Image originale

Il va s’agir de la redresser, la recadrer, et la nettoyer. Allez go !

Redresser son image

Dans un premier temps, il faut la redresser, ce sera tout de suite bien moins fatiguant à déchiffrer. Dans Gimp, allez dans le menu Image>Transformer>Rotation 90° Horaire (ou anti-horaire suivant votre cas :p).

Image originale

Vous pouvez également utiliser l’outil de rotation (entouré en rouge) pour corriger un angle plus exotique !

Recadrer son image

Notre image est maintenant dans le bon sens, ouf !
Mais on a encore des bordures disgracieuses qu’il va falloir retirer avec l’outil de recadrage, en forme de lame de cutter :

Recadrage

Cela a en plus comme avantage d’alléger le poids en octets de votre image, ce qui est toujours important pour le net.

Dé-saturer son image

Une étape intermédiaire et optionnelle est de dé-saturer son image. En effet, on travail fréquemment sur des fichier en noir et blancs, mais des couleurs additionnelles sont ajoutées lors de la numérisation par scanner ou par l’appareil photo, comme c’est le cas ici.

Nous allons donc simplement passer l’image en niveaux de gris via le menu Couleurs>Désaturer.

Dé-saturer 1

Sélectionner l’option qui vous semble donner le meilleur rendu

Dé-saturer 2

Nettoyer son image

Il ne reste plus qu’un dernier détail à régler. Notre image est encore terne, avec de nombreuses tâches et bavures.

Nous allons régler cela simplement en réglant les niveaux de couleurs dans Couleurs>Niveaux.

Niveaux 1

Cet outil est relativement simple à utiliser. Vous voilà devant un histogramme à 3 curseur.

  • Celui de gauche, en noir, permet de régler la base noir. Tout le gris ce trouvant au delà du curseur sera supprimé et remplacé par du noir.
  • Le curseur de droite a l’effet inverse, et il est le plus important : il supprime tous les tons de gris clair pour les transformer en blanc.
  • Le dernier curseur au milieu sert simplement à éclaircir ou assombrir l’ensemble des couleurs restantes suivant que le déplace vers le noir à gauche, ou le blanc à droite.

Encore une fois, cette étape va énormément alléger le poids en octet de votre image, puisque toutes les informations de couleurs superflues vont être supprimées ! 😉

Niveaux 2

Voilà, vous pouvez à présent fièrement présenter votre travail sur le net !

Image finale

nb : ici, on constate encore une zone sombre sur les bords. Cette zone est dû à la numérisation via un appareil photo. Pour la retirer, il faudrait soit détourer l’image grâce à l’outil « chemins » utilisant des courbes de Bézier, soit utiliser l’outil « baguette magique », mais il ne s’agit ici que d’un tutoriel. Ce problème ne se pose pas pour les image scannées.

Requête natives avec Symfony 2 et doctrine.

Quand on est développeur, à chaque fois qu’on résout un problème, c’est pour en trouver un autre derrière.

Je sortais à peine de ma dernière galère que voilà que j’ai replongé dans une nouvelle : les requête avec Doctrine.

Oui, parce que Doctrine, c’est beau, ça brille, ça donne envie. Mais… Non. Le nouveau constructeur de requête ne tiens pas ses promesses et on atteint très très vite ses limites. Contrairement à son ancienne version qui montrait une grande robustesse et une grande simplicité, vous aurez probablement très vite besoin d’utiliser avec la version 2 ce qu’on appelle des « requêtes natives » (native query dans la langue de Shakespeare).

mais bon, comme vous vous en foutez un peu de ma vie, ne prolongeons pas plus cette introduction.

Le constructeur de requêtes

Doctrine nous offre comme outil principal le constructeur de requête (query builder). Il est très simple et fonctionne très bien pour des requête basique :

$query = $this->createQueryBuilder('u')
->addSelect("SUM(CASE WHEN kComp.slug is not null THEN 1 ELSE 0 END)")
->leftJoin('u.keywords', 'kComp', \Doctrine\ORM\Query\Expr\Join::WITH, '1 = 1')
->where('u.id <> :userId')
->groupBy('u.id')
->orderBy('u.lastLogin', 'ASC')
->setParameter('userId', $_search->getUser()->getId())
;

Donc comme on peut le voir, ça fourni quand même toutes les fonctionnalités de bases :

  • ajout de champs personnalisés,
  • jointures strictes via innerJoin ou jointures flottantes via leftJoin, avec possibilités d’ajouter des conditions supplémentaire via le mot clef « WITH »
  • les conditions where
  • les classiques « order by »
  • et enfin l’utilisation de paramètres personnalisés avec la méthode « setParameter » qui remplacera automatiquement les variables par leurs valeurs.

Seulement, de nombreuse fonction ne sont pas reconnues, comme par exemple le « IF » mysql. De plus, Doctrine ne permet pas (plus) d’écraser les conditions ON ce qui s’avère beaucoup plus bloquant.

C’était pourtant ce qui rendaient Doctrine 1 vraiment plaisant à utiliser, on pouvait simplement écraser les jointures ON avec « ->innerJoin(‘u.Relation ON u.condition = « new_condition »‘« . Ce n’est malheureusement plus possible.

Alors comment faire quand on veut exécuter une requête un poil plus intéressante ? Et bien c’est le chapitre suivant…

Les requêtes natives

Doctrine nous fournie pour se faire la possibilité de lancer directement des requêtes natives. Malheureusement, elles sont beaucoup moins « user friendly », surtout si vous utilisez Symfony 2.3 ou inférieur.

La première chose à faire dans ce cas, c’est de mettre à jour l’ORM vers la version 2.4. En effet, Symfony utilise une version inférieur de l’ORM par défaut, et elle est bourrée de bugs au niveau des requêtes natives, ce qui les rend inutilisables.

Ouvrez donc le fichier composer.json à la racine de votre projet et modifiez-le pour redéfinir les dépendances tel que :

"require": {
...,
"doctrine/orm": "~2.4",
...,

Ensuite, ouvrez un terminal et lancez la commande ./composer.phar update pour effectuer la mise à jour de la nouvelle version de l’ORM Doctrine.

Maintenant, nous pouvons enfin créer notre requête native :

// init vars
$where = '';
$parameters = array();

// tout d'abords, nous créons le builder pour mapper les résultats en entités doctrine compréhensible :
$rsm = new ResultSetMappingBuilder($this->_em);
$rsm->addRootEntityFromClassMetadata('My\UserBundle\Entity\User', 'u'); // on définie l'entité de base qui apparaîtra dans le FROM
$rsm->addScalarResult('compatibility', 'compatibility'); // on déclare notre champ personnalisé
$rsm->addJoinedEntityFromClassMetadata('My\UserBundle\Entity\Keyword', 'kComp', 'u', 'keywords', array(
'id' => 'kCompid',
'title' => 'kComptitle',
)); // et enfin, on ajoute les relations que l'on désire récupérer. Notez qu'il faut obligatoirement renommer manuellement les champs qui ont le même nom que ceux que l'on pourrait trouver dans l'entité principale, ici "id" et "title".

// maintenant, nous allons définir des conditions manuellement
if ($_search->getTitle() !== null) {
$where .= ' AND u.title = :title';
$parameters['title'] = $_search->getTitle();
}

// On rédige la requête en sql. On insère l'objet de mapping des résultat en tant que select et les conditions WHERE. Remarquez qu'on peut maintenant tout à fait écraser les jointures et en créer plusieurs.
$sql = 'SELECT ' . $rsm . ', SUM(CASE WHEN kComp.slug = kSearch.slug THEN 1 ELSE 0 END) as compatibility
FROM `fos_user` u
LEFT JOIN keyword kSearch ON kSearch.user_id = :userId
LEFT JOIN keyword kComp ON kComp.user_id = u.id AND kComp.slug = kSearch.slug
WHERE u.id <> :userId ' . $where . '
GROUP BY u.id
ORDER BY u.last_activity ASC'
;

// Il ne reste plus qu'à créer l'objet de requête native et à lui passer les paramètres qu'on a défini plus haut.
$query = $this->_em->createNativeQuery($sql, $rsm);
$query->setParameters($parameters);
$query->setParameter('userId', $_search->getUser()->getId());

Voilà, maintenant vous pouvez récupérer le résultat de votre requête correctement hydraté en objet Doctrine !


Remerciement à FrozenFire du channel irc #doctrine pour son aide 😉

 

Accéder au repository depuis votre entity dans Symfony 2

… C’est impossible. Et oui, moi aussi ça m’a fait un choc.

Pourtant, mon problème initial me semblait particulièrement simple et logique : j’ai une entité User qui possède plusieurs entités Pictures, et je voulais créer une propriété personnalisée User->getAvatar() récupérant une seule entité Picture qui aurait été définie comme « isMain ».

Seulement non. on ne peut pas faire :

<?php // Entity\User
public function getAvatar() {
	return $this->getRepository('UserBundle:Picture')->getUserAvatar($this->id)->getQuery()->getSingleResult();
}

et donc on ne peut pas non plus faire dans twig :

{{ user.avatar }}

… Alors comment allons nous faire ?

Et bien j’aimerai vous dire que c’est simple. Mais ça ne l’ai pas.

Le tutoriel

Il faut créer un service UserManager et une extension twig, en plus bien sûr de notre fonction dans le repository.

Commençons par notre fonction dans le répository PictureRepository :

<?php // Entity\PictureRepository :
public function getUserAvatar($_id) {
	$query = $this->createQueryBuilder('p')
		->where('p.user = :userId')
		->setParameter(':userId', $_id)
		->orderBy('p.isMain', 'DESC') // j'utilise OrderBy dans le cas ou il y aurais effectivement des pictures associée, mais aucune définie comme main
	;

	return $query;
}

Voilà pour la fonction qui va nous permettre de récupérer l’avatar. Remarquez que l’on renvoie seulement la requête, et non son résultat. Cela permet de pouvoir réutiliser la même requête ailleurs si on a besoin d’y ajouter de nouvelles conditions par exemple.

Nous allons maintenant créer le service UserManager qui va se charger d’appeler les fonctions des répositories :

<?php // Entity\UserManager

namespace MyBundle\Entity;

use Doctrine\ORM\EntityManager;
use MyBundle\Entity\User;

class UserManager {

	protected $em;

	public function __construct(EntityManager $em) {
		$this->em = $em;
	}

	/**
	* return Picture is main for user
	* @param MyBundle\Entity\User $user
	* @return MyBundle\Entity\Picture
	*/
	public function getAvatar(User $user) {
		$avatar = $this->em->getRepository('MyBundle:Picture')->getUserAvatar($user->getId())->setMaxResults(1)->getQuery()->getOneOrNullResult();
		return !$avatar ? new Picture() : $avatar;
	}

}

Dans notre service, nous avons donc bien la fonction getAvatar() qui va aller chercher notre méthode getUserAvatar créée plus tôt. Remarquez également que notre constructeur prend en paramètre le manager d’entité de notre application.

Pour cela, il faut informer symfony que notre nouvelle classe UserManager est bien un service :

# MyBundle\Resources\config\services.yml
services
  my_user.user_manager:
    class: MyBundle\Entity\UserManager
    arguments: ["@doctrine.orm.entity_manager"]

Notre classe UserManager sera maintenant accessible dans les autres service et dans vos contrôleurs.

Il ne nous reste plus qu’à créer notre extension twig pour pouvoir accéder à notre objet Avatar dans nos templates :

<?php // MyBundle\Twig

namespace MyBundle\Twig;

use MyBundle\Entity\UserManager;

class UserExtension extends \Twig_Extension {

	protected $manager;

	public function __construct(UserManager $manager) {
		$this->manager = $manager;
	}

	public function getFunctions() {
		return array(
			'avatar' => new \Twig_Function_Method($this, 'avatarFunction'),
		);
	}

	/**
	* Function rendering the avatar Picture object of user
	* @param User $user
	* @return Picture
	*/
	public function avatarFunction($user) {
		return $this->manager->getAvatar($user);
	}

	public function getName() {
		return 'user_extension';
	}

}

Et finallement, on en fait également un service pour que Twig soit informé de cette nouvelle extention :

my_user.user_extension:
  class: MyBundle\Twig\UserExtension
  arguments: ["@my_user.user_manager"]
  tags:
    - { name: twig.extension }

Enfin, notre objet « Avatar » est complètement accessible, y compris dans Twig, et nous pouvons accéder à ses propriétés, comme le chemin vers le fichier image de notre Picture :

<img alt="" src="{{ avatar(user).webpath }}" />

Mes conclusions

Tout d’abord, force est de constater que le code est considérablement alourdi. On choisis d’utiliser un framework comme Symfony pour gagner du temps sur le développement, mais quand on voit ça, on ne peut que ce demander si on y arrive vraiment… On vient d’écrire plus de 30 lignes de code pour une fonction qui en prend normalement 3, et ce dans 4 fichiers différents…

De plus, ça rend également plus complexe la maintenabilité du code… Je n’ose imaginer ce qu’il se passerait si jamais on devait avoir des mises à jours à ce niveau.

Enfin je reviens sur le cœur du problème. Symfony nous impose ce système en prétendant que les entités ne doivent être utilisées uniquement pour représenter strictement le schéma de la base de donnée. Hors sur ce point, on voit qu’ils se contredisent eux-même dans leurs tutoriels en y créant de méthodes personnalisées : http://symfony.com/doc/current/cookbook/doctrine/file_uploads.html.

Quelle est donc la différence entre créer une méthode « getWebPath » et une méthode « getAvatar » qui fait appel à un repository ?

Vous me répondrez peut-être qu’un repository fait appel à la base de donnée via une requête.

Il me semble que c’est pourtant la fonction première d’une entité. Toutes ses propriétés résultent d’un appel à la BDD.

C’est le genre de choses qui étaient tellement simple avec Symfony 1 et qui font de Symfony 2 un véritable enfer.

Note complémentaire

Si ce code est fonctionnel, notez qu’il reste encore un hic : à chaque fois que vous appellerez votre nouvelle fonction avatar(), Doctrine va à nouveau exécuter la même requête, ce qui est inutile. Pour éviter cela, vous pouvez soit utiliser le cache de résultat fourni par Doctrine, soit créer quand même une propriété publique avatar à votre objet user et la remplir lors du premier appel de votre service UserManager->getAvatar().


Mes plus chaleureux remerciement à amenophis du chan #symfony sur IRC qui m’a expliqué le truc 😉

Jitsi

Depuis mon passage au CRI de l’UVSQ, je suis devenu un fervent défenseur des logiciels libres.

Je vais aujourd’hui vous parler de Jitsi, l’équivalent libre de Skype.

Pourquoi devrait-on abandonner Skype ?

Skype rempli effectivement ses fonctions. En effet, vous pouvez chatter, démarrer des conversations audios avec un ou plusieurs interlocuteurs, partager votre écran ou bien effectuer des visioconférences.

Mais skype, ce n’est pas que ça. C’est une vraie boite noire. Personne ne sais exactement ce qu’il fait, mais… On sait à quoi il accède. Notamment à tous vos historiques web, ce qui lui permet de connaître votre vrai nom, vos adresses mails, etc. Bref, tout ce dont a besoin un bon spammeur. D’ailleurs, Skype ne s’en cache pas vraiment, puisque vous acceptez qu’il récolte toutes ces données quand vous acceptez les CGU de skype.

Si ce côté technique ne vous pré-occupe pas des masses, l’autre point sensible est l’éthique plus que douteuse de Skype. Skype est une boite noire. Ce ne sont pas les conversations qui sont codées, mais le logiciel lui-même. Tout ce que vous dites ou écrivez reste parfaitement lisible par les employés de Skype qui possèdent la clé de décryptage. Cela entraîne quelques problèmes pour les utilisateurs ayant la malchance de vivre dans des pays comme la Chine ou la Russie

Les solutions

C’est pour cela que des petits gars bien sympathiques et très débrouillards ont créé leurs propres logiciels avec un code source ouvert !
L’intérêt d’un code source ouvert, c’est qu’on sait parfaitement ce que fait un logiciel.
Vous trouverez une liste des logiciels libres équivalent à Skype sur l’excellent site de framasoft.

Jitsi

Parmi l’ensemble des logiciels de VOIP, mon choix se porte sur Jitsi. Offrant toutes les fonctionnalités de Skype si on utilise un compte gmail ou SIP (via iptel ou ekiga par exemple), il est également un agrégateur de comptes de communication.

En effet, vous pouvez gérer en simultané à la fois votre compte gmail, msn, sip, facebook et j’en passe, la liste est longue !

Ce logiciel développé en Java est disponible sur toutes les plateformes, que vous utilisiez un système Windows, Mac, Linux ou Unix et permet une communication transparente entre chacun.

Léger et facile d’utilisation, ce logiciel ne pourra que vous satisfaire 😉