Tauri V2

Présentation

Présentation

Il y a quelques temps, j’avais voulu faire un test de Tauri dans sa nouvelle version (V2) mais mon PC n’étant pas à jour (hum …), il manquait de nombreuses librairies … Mais maintenant qu’il est à jour : on peut se lancer.

Mise en place

Pour les premiers pas, on va faire simple : on va suivre la documentation.

Dans l’ordre : création du projet.

npm create tauri-app@latest

Comme d’habitude avec ce genre de commande, cela pose plusieurs questions : nom du projet, identifiant, le langage (j’ai choisi Typescript), packagemanager (npm), template (svelte est directement présent \o/) …
Sur le langage, j’ai vu que .Net était disponible. C’est un framework que j’ai utilisé dans le passé, à tester :)

La commande donne les commandes suivantes :

  cd monprojet
  npm install
  npm run tauri android init

Voici le résultat de la dernière commande :

npm run tauri android init

> monprojet-app@0.1.0 tauri
> tauri android init

    Info Using installed NDK: /opt/apps/ide/android/SDK/ndk/29.0.14206865
    Generating Android Studio project...
    Info "../src-tauri" relative to "../src-tauri/gen/android/monprojet_app" is "../../../"
    victory: Project generated successfully!
    Make cool apps! 🌻 🐕 🎉

La commande suivante permet de lancer l’application sur le téléphone :

npm run tauri android dev

La première fois c’est TRES TRES TRES long. Le projet télécharge les différentes dépendances Rust, compile plusieurs choses et finalement déploie l’application.

Je crois que pour la première fois depuis longtemps, j’ai quelques choses qui fonctionne du premier coup. J’ai bien une application Android sur mon téléphone et quand je regarde le code c’est bien du Svelte (SvelteKit) avec du Rust dedans :)

A noter qu’avant même de coder quelque chose, le projet prend 3 Go … dont 2.9 lié à la partie build rust*

Première visite

L’application SveleKit est directement configuré en mode static :

  • le bon adapter est déployé : [@sveltejs/adapter-static][https://svelte.dev/docs/kit/adapter-static],
  • La layout est configuré :
    export const ssr = false;
    Par contre, je suis un peu étonné car normalement pour le static, il faut également indiquer
    export const prerender = true;
    On verra bien.

Niveau lien avec Rust, il y a petit exemple :

  // On notera que le code est bien Svelte 5 avec les $state()
  import { invoke } from "@tauri-apps/api/core";

  let name = $state("");
  let greetMsg = $state("");

  async function greet(event: Event) {
    event.preventDefault();
    greetMsg = await invoke("greet", { name });
  }

Pour trouver la trace de cette commande “greet”, il faut aller dans le code qui est présent dans src-tauri (celui prend plusiers Go 😄) et spécialement dans le fichier lib.rs :

#[tauri::command]
fn greet(name: &str) -> String {
    format!("Hello, {}! You've been greeted from Rust!", name)
}

On note le décorateur pour indiquer que c’est une commande Tauri.

La fonction est basique mais l’intégrer comme cela ne suffit pas, il faut également que Tauri, la connaisse et cela se fait dans la fonction run :

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .invoke_handler(tauri::generate_handler![greet])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

La ligne qui compte : .invoke_handler(tauri::generate_handler![greet]) mais également (encore une fois le décorateur)

Ajout d’une première commande

Juste pour valider le process, ajout d’une commande qui multiplie par deux un chiffre (on fait dans le fou fou !)

Le code Rust :

#[tauri::command]
fn double(nb: i32) -> i32 {
    nb * 2;
}

et dans svelte :

let derivedDouble = $derived(await doubleTheNumber());

async function doubleTheNumber() {
  return await invoke<number>("double", { nb: origin });
}

Quelques infos :

  • les paramètres doivent respecter le nom. Ici, le parmètre c’est nb donc il faut bien qu’il soit présent dans l’objet,
  • Comme les appels sont asynchrones, j’en ai profité pour tester une nouvelle feature de Svelte : await.

Gestion d’une photo

Lors de précédente étude, la prise de photo était un sujet utilisé pour apprendre. J’ai voulu faire pareil. J’ai été supris car je n’ai pas trouvé de plugin camera dans la liste des plugins “officiel”.

Pour mettre en place, le début a consisté à implémenter ce qui est présenté dans la MDN. Dès le début, la caméra par défaut sélectionné était la caméra avant mais cela ne m’a pas trop géné pour les tests.

Par contre, quand j’ai voulu mettre un sélecteur de caméra => impossible de démarrer le flux avant.

Après quelques recherches, j’ai cru comprendre que cela pouvait venir d’un souci de possibilité de la Webview utilisée … mais j’ai également vu qu’il existait un plugin pour lire des codes barres (un plugin pour lire les codes barres mais pas de plugin pour photo … )

J’ai essayé de comprendre le code (hum) et une ligne était présente dans le fichier AndroidManifest.xml:

<uses-feature android:name="android.hardware.camera.any"/>

Depuis l’ajout de cette ligne … ca fonctionne … de là voir un lien :)

En vrai … ca dépend … parfois, ca marche … parfois non … pas très stable

Utilisation d’un plugin

Mise en place

Tauri propose différents plugins officiels + communautaires. Dans la liste, il y a un plugin SQL qui pourrait être intéressant pour stocker les photos.

Les commandes :

npm run tauri add sql # pour installer le plugin
cd src-tauri
cargo add tauri-plugin-sql --features sqlite # ajout de sqlite car c'est celle que je veux utiliser

La première commande masque beaucoup d’actions :

  • Ajout du package dans le package.json,
  • Idem au niveau des dépéndances Cargo (rust),
  • Enregistrement des autorisations,
  • Enregistrement du plugin.

La deuxième commande modifie l’enregistrement de la feature :

tauri-plugin-sql = { version = "2", features = ["sqlite"] }

Premier accès

Pour commencer et faire quelques tests, c’est layout.js qui porte l’accès à la base :

 try {
        console.log("LOAD");
        const db = await Database.load('sqlite:test.db');
        console.log(db.path);
        const result = await db.execute(
            'SELECT * FROM PHOTOS'
        );
        console.log(result);
    } catch (error) {
        console.error(error)
    }

La première erreur indiquait qu’il manquait des permissions (sql:allow-execute). Il semble que Tauri ait mis en place un système de persmission afin de s’assurer que le frontend ne fasse pas n’importe quoi. Il faut modifier le fichier src-tauri/capabilities/default.json :

  "permissions": [
    "core:default",
    "opener:default",
    "sql:default",
    "sql:allow-execute",
    "sql:allow-select"
  ]

Et donc là j’ai l’erreur “no such table: PHOTOS” ce qui est assez normal :)

Migration

Comme l’indique la documentation, il existe un système de migrations directement intégré. Afin de le simplifier, j’ai fait quelques adaptations :

  • Comme donner en exemple, les fichiers sql sont externalisés dans un répertoire ./migrations,
  • Création d’un module externe pour enregistrer,
  • Mise à jour du fichier libs.rs pour gérer l’appel aux migrations.

Avec tout cela, j’ai un bien un retour qui me dit que la table existe mais qu’il n’y a pas de données.

Externalisation

Pour le moment, le fichier de base de données est crée dans la zone “sandbox”. La création de lignes m’a permis de vérifier que les données étaient persistées d’une exécution à l’autre mais j’aurais aimé réussir à ce que le fichier soit stocker dans un répertoire spécifique accessible via USB.

Malgré plusieurs tentatives, je n’ai pas réussi … Toujours un souci de droit, de récupération de données … Bref …

Partage du nom de fichier

Pour le moment, le chemin de la base de données est connue à deux endroits :

  • dans le code JS : const db = await Database.load('sqlite:test.db');,
  • dans le code Rust : add_migrations("sqlite:test.db", migrations::get_migrations())
    Ce que je n’aime pas …

C’est assez simple au final. Il suffit d’ajouter une commande :

static SQL_PATH: &str = "sqlite:test.db";

#[tauri::command]
fn sql_path() -> String {
    return SQL_PATH.to_string();
}

et l’utiliser dans la partie migration :
.add_migrations(SQL_PATH, migrations::get_migrations())

et en JS :

const sql_path = await invoke<string>("sql_path", {});
const db = await Database.load(sql_path);
let result = await db.select(
    'SELECT * FROM photos'
);
console.log(result);

Stockage des photos

L’idée serait maintenant de stocker les photos dans la base de données :). En premier, une simple classe de service qui permet de faire des insert, update et delete. Rien de bien compliquer … juste le temps de le faire (aurais-du faire de l’IA :o)

En deuxième mettre à jour le store … bon ca servait à rien c’était plus du JS que du Tauri :)

Enregistrement physique des photos

Intro

Elles sont stockées en base mais j’aimerais les enregistrer dans une gallerie de l’appareil. Au regard de mon souci sur la base de données, je ne suis pas optimiste :).

Lors de mes recherches, j’ai trouvé quelques liens qui expliquent mieux certaines de mes difficultés :

A priori, cela doit être possible d’enregistrer mais en respectant certains répertoires

Mise en place

Le début est comme un plugin classique :

npm run tauri add fs

qui est suivi de la mise à jour des capacités :

{
  "identifier": "fs:scope",
  "allow": [{ "path": "$PICTURE/**/*" }]
},
{
  "identifier":"fs:allow-app-write",
  "allow": [{ "path": "$PICTURE/**/*" }]
}

Premier test

const res = await exists('avatar.jpg', { baseDir: BaseDirectory.Picture });
console.log(res);

let resCreate = await create("test.txt", { baseDir: BaseDirectory.Picture })
console.log(resCreate);

resCreate = await create("test2.txt", { baseDir: BaseDirectory.Picture })
console.log(resCreate);

Il m’a fallu du temps pour comprendre que le répertoire Picture n’était pas celui directement accessible depuis la connexion USB mais ceux de mon application. La documentation permet d’avoir une idée des répetoires via path.

Il semble qu’écrire ailleurs demande des droits spécifiques … donc pour le moment, on va en rester là.

Ecriture de la photo

Une petite fonction pour écrire un blob :

  /**
   * Enregistre un fichier
   * @param name Nom du fichier
   * @param data Les données du fichier
   */
  public static async saveFile( name: string, blob: Blob) {

      // Calcul des noms
      const filename = `${ name }_rename.jpg`;
      const imagePath =`${ await path.pictureDir() }${ path.delimiter }${ filename }`; 

      // Test si existe
      const resExist = await exists(name, { baseDir: BaseDirectory.Picture });
      if(resExist) {
          throw new Error(`${ filename } existe déjà dans ${ path }`);
      }

      // Préparation des données
      const arrayBuffer = await blob.arrayBuffer();
      const uint8Array = new Uint8Array(arrayBuffer);


      // Ecrire
      await writeFile(name, uint8Array, {
          baseDir: BaseDirectory.Picture,
      });
      // --> pour corriger un souci dre reconnaissance du type
      await rename(name, filename, { oldPathBaseDir: BaseDirectory.Picture, newPathBaseDir: BaseDirectory.Picture })

      return imagePath;
  } // /saveFile

Et voilà.

J’ai donc mon image qui est stocké en base64 dans la base de données et stocker sur le DD de mon téléphone.

Bilan

Pour un premier petit tour avec Tauri, c’est déjà pas mal.

Globalement c’est positif. La difficulté est de réussir à combiner les difficultés du framework Svelte, de Tauri, Rust (pas simple) et pour le coup Android.

Comme cela ne supporte pas le Web, je pense que je vais rester sur Capacitor encore quelques temps.