Excalidraw et Fugu: améliorer le parcours utilisateur principal

Toute technologie suffisamment avancée est indiscernable de la magie. sauf si vous le comprenez. Je m'appelle Thomas Steiner, je travaille dans l'équipe Developer Relations chez Google. Dans cet article, qui reprend ma présentation à la Google I/O, je vais examiner certaines des nouvelles API Fugu et la façon dont elles améliorent les parcours utilisateur principaux dans l'application Web progressive Excalidraw. Vous pourrez ainsi vous inspirer de ces idées et les appliquer à vos propres applications.

Comment j'ai découvert Excalidraw

Je vais commencer par une histoire. Le 1er janvier 2020, Christopher Chedeau, ingénieur logiciel chez Facebook, a tweeté à propos d'une petite application de dessin sur laquelle il avait commencé à travailler. Cet outil vous permet de dessiner des boîtes et des flèches qui ont l'air de dessins animés et de dessins à la main. Le lendemain, vous pourrez également dessiner des ellipses et du texte, ainsi que sélectionner des objets et les déplacer. Le 3 janvier, l'application a reçu son nom, Excalidraw, et, comme pour tout bon projet parallèle, l'achat du nom de domaine a été l'une des premières actions de Christopher. Vous pouvez désormais utiliser des couleurs et exporter l'ensemble du dessin au format PNG.

Capture d'écran de l'application prototype Excalidraw montrant qu'elle prend en charge les rectangles, les flèches, les ellipses et le texte.

Le 15 janvier, Christopher a publié un article de blog qui a suscité beaucoup d'attention sur Twitter, y compris la mienne. Le post commençait par quelques statistiques impressionnantes :

  • 12 000 utilisateurs actifs uniques
  • 1 500 étoiles sur GitHub
  • 26 contributeurs

Pour un projet qui a commencé il y a à peine deux semaines, ce n'est pas mal du tout. Mais ce qui a vraiment piqué ma curiosité se trouvait plus bas dans le post. Christopher a écrit qu'il avait essayé quelque chose de nouveau cette fois-ci : donner à tous ceux qui ont déposé une demande d'extraction un accès inconditionnel aux commits. Le jour même où j'ai lu l'article de blog, j'ai envoyé une demande d'extraction qui ajoutait la prise en charge de l'API File System Access à Excalidraw, ce qui répondait à une demande de fonctionnalité qu'une personne avait déposée.

Capture d'écran du tweet dans lequel j'annonce mon RP.

Ma demande d'extraction a été fusionnée le lendemain, et j'ai ensuite bénéficié d'un accès complet aux commits. Inutile de préciser que je n'ai pas abusé de mon pouvoir. Et aucun des 149 contributeurs à ce jour ne l'a fait non plus.

Aujourd'hui, Excalidraw est une application Web progressive installable à part entière, avec une prise en charge hors connexion, un mode sombre époustouflant et, oui, la possibilité d'ouvrir et d'enregistrer des fichiers grâce à l'API File System Access.

Capture d'écran de l'état actuel de l'application Web progressive Excalidraw.

Lipis explique pourquoi il consacre autant de temps à Excalidraw

C'est la fin de mon histoire sur la façon dont je suis arrivé à Excalidraw, mais avant de me plonger dans certaines des fonctionnalités étonnantes d'Excalidraw, j'ai le plaisir de vous présenter Panayiotis. Panayiotis Lipiridis, plus connu sous le nom de lipis sur Internet, est le contributeur le plus prolifique d'Excalidraw. J'ai demandé à lipis ce qui le motivait à consacrer autant de temps à Excalidraw :

Comme tout le monde, j'ai découvert ce projet grâce au tweet de Christopher. Ma première contribution a été d'ajouter la bibliothèque Open Color, dont les couleurs font toujours partie d'Excalidraw aujourd'hui. Le projet ayant pris de l'ampleur et le nombre de demandes ayant augmenté, ma contribution suivante a consisté à créer un backend pour stocker les dessins afin que les utilisateurs puissent les partager. Mais ce qui me pousse vraiment à contribuer, c'est que tous ceux qui ont essayé Excalidraw cherchent des excuses pour l'utiliser à nouveau.

Je suis tout à fait d'accord avec lipis. Quiconque a essayé Excalidraw cherche des excuses pour l'utiliser à nouveau.

Excalidraw en action

Je vais maintenant vous montrer comment utiliser Excalidraw en pratique. Je ne suis pas un grand artiste, mais le logo Google I/O est assez simple, alors je vais essayer. La boîte est le "i", la ligne peut être la barre oblique et le "o" est un cercle. Je maintiens la touche Maj enfoncée pour obtenir un cercle parfait. Je vais déplacer un peu la barre oblique pour que ça ait l'air mieux. Ajoutons maintenant de la couleur aux lettres "i" et "o". Le bleu, c'est bien. Peut-être un autre style de remplissage ? Entièrement plein ou hachuré ? Non, les hachures sont parfaites. Ce n'est pas parfait, mais c'est l'idée d'Excalidraw, alors je vais l'enregistrer.

Je clique sur l'icône d'enregistrement et saisis un nom de fichier dans la boîte de dialogue d'enregistrement. Dans Chrome, un navigateur compatible avec l'API File System Access, il ne s'agit pas d'un téléchargement, mais d'une véritable opération d'enregistrement, où je peux choisir l'emplacement et le nom du fichier, et où, si je le modifie, je peux simplement l'enregistrer dans le même fichier.

Je vais changer le logo et mettre le "i" en rouge. Si je clique à nouveau sur "Enregistrer", ma modification est enregistrée dans le même fichier qu'auparavant. Pour vous le prouver, je vais effacer le canevas et rouvrir le fichier. Comme vous pouvez le voir, le logo rouge et bleu modifié est de nouveau là.

Utiliser des fichiers

Sur les navigateurs qui ne sont pas compatibles avec l'API File System Access, chaque opération d'enregistrement est un téléchargement. Par conséquent, lorsque j'apporte des modifications, je me retrouve avec plusieurs fichiers dont le nom comporte un numéro incrémentiel, qui remplissent mon dossier "Téléchargements". Malgré cet inconvénient, je peux toujours enregistrer le fichier.

Ouvrir des fichiers

Quel est donc le secret ? Comment l'ouverture et l'enregistrement fonctionnent-ils sur différents navigateurs qui peuvent ou non prendre en charge l'API File System Access ? L'ouverture d'un fichier dans Excalidraw se fait dans une fonction appelée loadFromJSON)(, qui à son tour appelle une fonction appelée fileOpen().

export const loadFromJSON = async (localAppState: AppState) => {
  const blob = await fileOpen({
    description: 'Excalidraw files',
    extensions: ['.json', '.excalidraw', '.png', '.svg'],
    mimeTypes: ['application/json', 'image/png', 'image/svg+xml'],
  });
  return loadFromBlob(blob, localAppState);
};

La fonction fileOpen() provient d'une petite bibliothèque que j'ai écrite et appelée browser-fs-access, que nous utilisons dans Excalidraw. Cette bibliothèque fournit un accès au système de fichiers via l'API File System Access avec un ancien mécanisme de secours, ce qui lui permet d'être utilisée dans n'importe quel navigateur.

Commençons par l'implémentation lorsque l'API est prise en charge. Après avoir négocié les types MIME et les extensions de fichier acceptés, la pièce maîtresse consiste à appeler la fonction showOpenFilePicker() de l'API File System Access. Cette fonction renvoie un tableau de fichiers ou un seul fichier, selon que plusieurs fichiers sont sélectionnés ou non. Il ne reste plus qu'à placer le handle de fichier sur l'objet de fichier, afin qu'il puisse être récupéré à nouveau.

export default async (options = {}) => {
  const accept = {};
  // Not shown: deal with extensions and MIME types.
  const handleOrHandles = await window.showOpenFilePicker({
    types: [
      {
        description: options.description || '',
        accept: accept,
      },
    ],
    multiple: options.multiple || false,
  });
  const files = await Promise.all(handleOrHandles.map(getFileWithHandle));
  if (options.multiple) return files;
  return files[0];
  const getFileWithHandle = async (handle) => {
    const file = await handle.getFile();
    file.handle = handle;
    return file;
  };
};

L'implémentation de remplacement repose sur un élément input de type "file". Après la négociation des types MIME et des extensions à accepter, l'étape suivante consiste à cliquer de manière programmatique sur l'élément d'entrée afin d'afficher la boîte de dialogue d'ouverture de fichier. La promesse est résolue lorsque l'utilisateur a sélectionné un ou plusieurs fichiers.

export default async (options = {}) => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    const accept = [
      ...(options.mimeTypes ? options.mimeTypes : []),
      options.extensions ? options.extensions : [],
    ].join();
    input.multiple = options.multiple || false;
    input.accept = accept || '*/*';
    input.addEventListener('change', () => {
      resolve(input.multiple ? Array.from(input.files) : input.files[0]);
    });
    input.click();
  });
};

Enregistrer des fichiers

Passons maintenant à l'enregistrement. Dans Excalidraw, l'enregistrement s'effectue dans une fonction appelée saveAsJSON(). Il sérialise d'abord le tableau d'éléments Excalidraw en JSON, convertit le JSON en blob, puis appelle une fonction appelée fileSave(). Cette fonction est également fournie par la bibliothèque browser-fs-access.

export const saveAsJSON = async (
  elements: readonly ExcalidrawElement[],
  appState: AppState,
) => {
  const serialized = serializeAsJSON(elements, appState);
  const blob = new Blob([serialized], {
    type: 'application/vnd.excalidraw+json',
  });
  const fileHandle = await fileSave(
    blob,
    {
      fileName: appState.name,
      description: 'Excalidraw file',
      extensions: ['.excalidraw'],
    },
    appState.fileHandle,
  );
  return { fileHandle };
};

Commençons par examiner l'implémentation pour les navigateurs compatibles avec l'API File System Access. Les premières lignes semblent un peu complexes, mais elles ne font que négocier les types MIME et les extensions de fichier. Si j'ai déjà enregistré un fichier et que j'ai déjà un handle de fichier, aucune boîte de dialogue d'enregistrement ne doit s'afficher. Toutefois, s'il s'agit de la première sauvegarde, une boîte de dialogue de fichier s'affiche et l'application reçoit un handle de fichier pour une utilisation ultérieure. Le reste consiste simplement à écrire dans le fichier, ce qui se fait via un flux accessible en écriture.

export default async (blob, options = {}, handle = null) => {
  options.fileName = options.fileName || 'Untitled';
  const accept = {};
  // Not shown: deal with extensions and MIME types.
  handle =
    handle ||
    (await window.showSaveFilePicker({
      suggestedName: options.fileName,
      types: [
        {
          description: options.description || '',
          accept: accept,
        },
      ],
    }));
  const writable = await handle.createWritable();
  await writable.write(blob);
  await writable.close();
  return handle;
};

Fonctionnalité "Enregistrer sous"

Si je décide d'ignorer un descripteur de fichier existant, je peux implémenter une fonctionnalité "Enregistrer sous" pour créer un fichier à partir d'un fichier existant. Pour vous le montrer, je vais ouvrir un fichier existant, le modifier, puis ne pas l'écraser, mais créer un fichier à l'aide de la fonctionnalité "Enregistrer sous". Le fichier d'origine n'est pas altéré.

L'implémentation pour les navigateurs qui ne sont pas compatibles avec l'API File System Access est courte, car elle ne fait que créer un élément d'ancrage avec un attribut download dont la valeur est le nom de fichier souhaité et une URL blob comme valeur d'attribut href.

export default async (blob, options = {}) => {
  const a = document.createElement('a');
  a.download = options.fileName || 'Untitled';
  a.href = URL.createObjectURL(blob);
  a.addEventListener('click', () => {
    setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
  });
  a.click();
};

L'élément d'ancrage est ensuite cliqué de manière programmatique. Pour éviter les fuites de mémoire, l'URL du blob doit être révoquée après utilisation. Comme il s'agit simplement d'un téléchargement, aucune boîte de dialogue d'enregistrement de fichier ne s'affiche jamais, et tous les fichiers sont placés dans le dossier Downloads par défaut.

Glisser-déposer

L'une de mes intégrations système préférées sur ordinateur est le glisser-déposer. Dans Excalidraw, lorsque je dépose un fichier .excalidraw dans l'application, il s'ouvre immédiatement et je peux commencer à le modifier. Sur les navigateurs compatibles avec l'API File System Access, je peux même enregistrer immédiatement mes modifications. Il n'est pas nécessaire de passer par une boîte de dialogue d'enregistrement de fichier, car le handle de fichier requis a été obtenu à partir de l'opération de glisser-déposer.

Le secret pour y parvenir est d'appeler getAsFileSystemHandle() sur l'élément data transfer lorsque l'API File System Access est compatible. Je transmets ensuite ce descripteur de fichier à loadFromBlob(), dont vous vous souvenez peut-être d'après les paragraphes précédents. Vous pouvez faire beaucoup de choses avec les fichiers : les ouvrir, les enregistrer, les réenregistrer, les faire glisser, les déposer. Mon collègue Pete et moi avons documenté toutes ces astuces et bien d'autres dans notre article. Vous pouvez le consulter si tout cela est allé un peu trop vite.

const file = event.dataTransfer?.files[0];
if (file?.type === 'application/json' || file?.name.endsWith('.excalidraw')) {
  this.setState({ isLoading: true });
  // Provided by browser-fs-access.
  if (supported) {
    try {
      const item = event.dataTransfer.items[0];
      file as any.handle = await item as any
        .getAsFileSystemHandle();
    } catch (error) {
      console.warn(error.name, error.message);
    }
  }
  loadFromBlob(file, this.state).then(({ elements, appState }) =>
    // Load from blob
  ).catch((error) => {
    this.setState({ isLoading: false, errorMessage: error.message });
  });
}

Partager des fichiers

Une autre intégration système actuellement disponible sur Android, ChromeOS et Windows est l'API Web Share Target. Je me trouve ici dans l'application Fichiers, dans mon dossier Downloads. Je vois deux fichiers, dont l'un porte le nom non descriptif untitled et un code temporel. Pour vérifier son contenu, je clique sur les trois points, puis sur "Partager". L'une des options qui s'affiche est Excalidraw. Lorsque j'appuie sur l'icône, je constate que le fichier ne contient à nouveau que le logo I/O.

Lipis sur la version Electron obsolète

Il y a une chose que vous pouvez faire avec les fichiers dont je ne vous ai pas encore parlé : double-cliquer dessus. En règle générale, lorsque vous double-cliquez sur un fichier, l'application associée au type MIME du fichier s'ouvre. Par exemple, pour .docx, il s'agirait de Microsoft Word.

Excalidraw proposait une version Electron de l'application qui prenait en charge ces associations de types de fichiers. Ainsi, lorsque vous double-cliquiez sur un fichier .excalidraw, l'application Excalidraw Electron s'ouvrait. Lipis, que vous avez déjà rencontré, était à la fois le créateur et le responsable de l'abandon d'Excalidraw Electron. Je lui ai demandé pourquoi il pensait qu'il était possible d'abandonner la version Electron :

Depuis le début, les utilisateurs demandent une application Electron, principalement parce qu'ils souhaitent ouvrir des fichiers en double-cliquant dessus. Nous avions également l'intention de mettre l'application sur les plates-formes de téléchargement d'applications. En parallèle, quelqu'un a suggéré de créer une PWA. Nous avons donc fait les deux. Heureusement, nous avons découvert les API Project Fugu, comme l'accès au système de fichiers, l'accès au presse-papiers, la gestion des fichiers et plus encore. En un seul clic, vous pouvez installer l'application sur votre ordinateur ou votre mobile, sans le poids supplémentaire d'Electron. Il a été facile de décider d'abandonner la version Electron, de se concentrer uniquement sur l'application Web et d'en faire la meilleure PWA possible. De plus, nous pouvons désormais publier des PWA sur le Play Store et le Microsoft Store. C'est énorme !

On pourrait dire qu'Excalidraw pour Electron n'a pas été abandonné parce qu'Electron est mauvais, pas du tout, mais parce que le Web est devenu suffisamment bon. J'aime !

Gestion des fichiers

Lorsque je dis que "le Web est devenu suffisamment bon", c'est grâce à des fonctionnalités comme la gestion des fichiers à venir.

Il s'agit d'une installation macOS Big Sur standard. Voyons maintenant ce qu'il se passe lorsque je clique avec le bouton droit sur un fichier Excalidraw. Je peux choisir de l'ouvrir avec Excalidraw, la PWA installée. Bien sûr, le double-clic fonctionne aussi, mais c'est moins spectaculaire à montrer dans une vidéo.

Comment ça marche ? La première étape consiste à indiquer au système d'exploitation les types de fichiers que mon application peut gérer. Pour ce faire, j'utilise un nouveau champ appelé file_handlers dans le fichier manifeste de l'application Web. Sa valeur est un tableau d'objets avec une action et une propriété accept. L'action détermine le chemin d'URL à partir duquel le système d'exploitation lance votre application. L'objet "accept" est une paire clé/valeur de types MIME et des extensions de fichier associées.

{
  "name": "Excalidraw",
  "description": "Excalidraw is a whiteboard tool...",
  "start_url": "/",
  "display": "standalone",
  "theme_color": "#000000",
  "background_color": "#ffffff",
  "file_handlers": [
    {
      "action": "/",
      "accept": {
        "application/vnd.excalidraw+json": [".excalidraw"]
      }
    }
  ]
}

L'étape suivante consiste à gérer le fichier au lancement de l'application. Cela se produit dans l'interface launchQueue où je dois définir un consommateur en appelant, eh bien, setConsumer(). Le paramètre de cette fonction est une fonction asynchrone qui reçoit le launchParams. Cet objet launchParams comporte un champ appelé "files" qui me permet d'obtenir un tableau de descripteurs de fichiers à utiliser. Je ne m'occupe que du premier et, à partir de ce descripteur de fichier, j'obtiens un blob que je transmets ensuite à notre vieil ami loadFromBlob().

if ('launchQueue' in window && 'LaunchParams' in window) {
  window as any.launchQueue
    .setConsumer(async (launchParams: { files: any[] }) => {
      if (!launchParams.files.length) return;
      const fileHandle = launchParams.files[0];
      const blob: Blob = await fileHandle.getFile();
      blob.handle = fileHandle;
      loadFromBlob(blob, this.state).then(({ elements, appState }) =>
        // Initialize app state.
      ).catch((error) => {
        this.setState({ isLoading: false, errorMessage: error.message });
      });
    });
}

Si cela a été trop rapide, vous pouvez en savoir plus sur l'API File Handling dans mon article. Vous pouvez activer la gestion des fichiers en définissant le flag de fonctionnalité expérimentale de la plate-forme Web. Elle devrait être disponible dans Chrome d'ici la fin de l'année.

Intégration du presse-papiers

L'intégration du presse-papiers est une autre fonctionnalité intéressante d'Excalidraw. Je peux copier l'intégralité de mon dessin ou seulement certaines parties dans le presse-papiers, ajouter un filigrane si je le souhaite, puis le coller dans une autre application. Il s'agit d'une version Web de l'application Paint de Windows 95.

Le fonctionnement est étonnamment simple. Tout ce dont j'ai besoin, c'est du canevas sous forme de blob, que j'écris ensuite dans le presse-papiers en transmettant un tableau à un élément avec un ClipboardItem avec le blob à la fonction navigator.clipboard.write(). Pour en savoir plus sur ce que vous pouvez faire avec l'API Clipboard, consultez cet article.

export const copyCanvasToClipboardAsPng = async (canvas: HTMLCanvasElement) => {
  const blob = await canvasToBlob(canvas);
  await navigator.clipboard.write([
    new window.ClipboardItem({
      'image/png': blob,
    }),
  ]);
};

export const canvasToBlob = async (canvas: HTMLCanvasElement): Promise<Blob> => {
  return new Promise((resolve, reject) => {
    try {
      canvas.toBlob((blob) => {
        if (!blob) {
          return reject(new CanvasError(t('canvasError.canvasTooBig'), 'CANVAS_POSSIBLY_TOO_BIG'));
        }
        resolve(blob);
      });
    } catch (error) {
      reject(error);
    }
  });
};

Collaboration avec d'autres personnes

Partager l'URL d'une session

Saviez-vous qu'Excalidraw disposait également d'un mode collaboratif ? Plusieurs personnes peuvent travailler ensemble sur le même document. Pour démarrer une session, je clique sur le bouton de collaboration en direct, puis je lance une session. Je peux partager facilement l'URL de la session avec mes collaborateurs grâce à l'API Web Share intégrée à Excalidraw.

Collaboration en direct

J'ai simulé une session de collaboration en local en travaillant sur le logo Google I/O sur mon Pixelbook, mon téléphone Pixel 3a et mon iPad Pro. Vous pouvez voir que les modifications que j'apporte sur un appareil se répercutent sur tous les autres.

Je peux même voir tous les curseurs se déplacer. Le curseur du Pixelbook se déplace de manière fluide, car il est contrôlé par un pavé tactile. En revanche, le curseur du téléphone Pixel 3a et celui de la tablette iPad Pro sautent, car je contrôle ces appareils en appuyant avec mon doigt.

Afficher l'état des collaborateurs

Pour améliorer l'expérience de collaboration en temps réel, un système de détection d'inactivité est même en cours d'exécution. Le curseur de l'iPad Pro affiche un point vert lorsque je l'utilise. Le point devient noir lorsque je passe à un autre onglet ou une autre application du navigateur. Et lorsque je suis dans l'application Excalidraw, mais que je ne fais rien, le curseur m'indique comme inactif, symbolisé par les trois Z.

Les lecteurs assidus de nos publications pourraient penser que la détection d'inactivité est réalisée à l'aide de l'API Idle Detection, une proposition en phase préliminaire sur laquelle nous travaillons dans le cadre du projet Fugu. Alerte spoiler : ce n'est pas le cas. Bien que nous ayons implémenté cette API dans Excalidraw, nous avons finalement opté pour une approche plus traditionnelle basée sur la mesure du mouvement du pointeur et de la visibilité de la page.

Capture d&#39;écran des commentaires sur la détection d&#39;inactivité déposés sur le dépôt WICG Idle Detection.

Nous avons envoyé des commentaires pour expliquer pourquoi l'API Idle Detection ne répondait pas à notre cas d'utilisation. Toutes les API Project Fugu sont développées de manière ouverte. Tout le monde peut donc participer et faire entendre sa voix.

Lipis sur ce qui freine Excalidraw

À ce propos, j'ai posé une dernière question à lipis sur ce qui, selon lui, manque à la plate-forme Web et qui freine Excalidraw :

L'API File System Access est géniale, mais vous savez quoi ? La plupart des fichiers qui m'intéressent aujourd'hui se trouvent dans mon Dropbox ou mon Google Drive, et non sur mon disque dur. Je souhaiterais que l'API File System Access inclue une couche d'abstraction permettant aux fournisseurs de systèmes de fichiers distants tels que Dropbox ou Google de s'intégrer et que les développeurs puissent coder. Les utilisateurs peuvent alors se détendre et savoir que leurs fichiers sont en sécurité avec le fournisseur de services cloud de leur choix.

Je suis tout à fait d'accord avec lipis, je vis aussi dans le cloud. J'espère que cette fonctionnalité sera bientôt implémentée.

Mode application à onglets

Impressionnant ! Nous avons vu de très belles intégrations d'API dans Excalidraw. Système de fichiers, gestion des fichiers, presse-papiers, partage Web et cible de partage Web. Mais il y a une dernière chose. Jusqu'à présent, je ne pouvais modifier qu'un seul document à la fois. Plus maintenant. Découvrez pour la première fois une version préliminaire du mode application à onglets dans Excalidraw. Voici à quoi cela ressemble.

J'ai un fichier ouvert dans la PWA Excalidraw installée et exécutée en mode autonome. J'ouvre un nouvel onglet dans la fenêtre autonome. Il ne s'agit pas d'un onglet de navigateur classique, mais d'un onglet PWA. Dans ce nouvel onglet, je peux ensuite ouvrir un fichier secondaire et travailler dessus indépendamment de la fenêtre de l'application.

Le mode application à onglets est encore en phase de développement. Tout n'est donc pas encore figé. Si vous êtes intéressé, assurez-vous de lire l'état actuel de cette fonctionnalité dans mon article.

Conclusion

Pour ne rien manquer de cette fonctionnalité et d'autres, assurez-vous de consulter notre outil de suivi des API Fugu. Nous sommes très heureux de faire progresser le Web et de vous permettre d'en faire plus sur la plate-forme. Nous espérons qu'Excalidraw continuera de s'améliorer et que vous créerez des applications incroyables. Commencez à créer sur excalidraw.com.

J'ai hâte de voir certaines des API que je vous ai présentées aujourd'hui apparaître dans vos applications. Je m'appelle Tom et vous pouvez me trouver sous le nom d'utilisateur @tomayac sur Twitter et sur Internet en général. Merci beaucoup de votre attention et profitez bien du reste de Google I/O.