wheatandcatの開発ブログ

React Nativeで開発しているペペロミア & memoirの技術系記事を投稿してます

Tauriで保存データのインポート/エクスポート機能を作成 + Window Menuの設定

以下の記事から引き続き、TODOアプリを作成中。 www.wheatandcat.me

Window Menuの改修をしたので記載。

PR

github.com

実装

現状ローカルストレージにTODOのデータを保存しているのみの構成。
なので、アプリ削除で全てのデータが消えしまうので保存データのインポート/エクスポート機能を作成した。

デスクトップアプリだと、インポート/エクスポート機能はWindow Menuから選択して実行するパターンが多い
まずは、以下のドキュメントを参考にWindow Menuを修正。

tauri.app

以下のRustのファイルを修正してWindow Menuを変更した。

src-tauri/src/main.rs

fn main() {
    let context = tauri::generate_context!();
    let about = CustomMenuItem::new("about".to_string(), "About");
    let update = CustomMenuItem::new("update".to_string(), "Check for updates");
    let quit = CustomMenuItem::new("quit".to_string(), "Quit");
    let export = CustomMenuItem::new("export".to_string(), "Export").accelerator("Cmd+Shift+E");
    let import = CustomMenuItem::new("import".to_string(), "Import").accelerator("Cmd+Shift+I");
    let mainmenu = Submenu::new(
        "Todo",
        Menu::new()
            .add_native_item(MenuItem::About(
                "todo".to_string(),
                AboutMetadata::default(),
            ))
            .add_native_item(MenuItem::Services)
            .add_native_item(MenuItem::Separator)
            .add_native_item(MenuItem::Services)
            .add_native_item(MenuItem::Separator)
            .add_native_item(MenuItem::Hide)
            .add_native_item(MenuItem::HideOthers)
            .add_native_item(MenuItem::ShowAll)
            .add_native_item(MenuItem::Separator)
            .add_native_item(MenuItem::Quit)
            .add_native_item(MenuItem::Separator)
            .add_item(about)
            .add_item(update)
            .add_native_item(MenuItem::Separator)
            .add_item(quit),
    );
    let filemenu = Submenu::new("File", Menu::new().add_item(export).add_item(import));
    let editmenu = Submenu::new(
        "Edit",
        Menu::new()
            .add_native_item(MenuItem::Undo)
            .add_native_item(MenuItem::Redo)
            .add_native_item(MenuItem::Cut)
            .add_native_item(MenuItem::Copy)
            .add_native_item(MenuItem::Paste)
            .add_native_item(MenuItem::SelectAll),
    );
    let screenmenu = Submenu::new(
        "Window",
        Menu::new()
            .add_native_item(MenuItem::EnterFullScreen)
            .add_native_item(MenuItem::Minimize)
            .add_native_item(MenuItem::Zoom)
            .add_native_item(MenuItem::CloseWindow),
    );

    let menu = Menu::new()
        .add_submenu(mainmenu)
        .add_submenu(filemenu)
        .add_submenu(editmenu)
        .add_submenu(screenmenu);

    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![greet])
        .menu(menu)
        .on_menu_event(|event| match event.menu_item_id() {
            "about" => {
                let window = event.window();
                window.emit("about", "about".to_string()).unwrap();
            }
            "export" => {
                let window = event.window();
                window.emit("export", "export".to_string()).unwrap();
            }
            "import" => {
                let window = event.window();
                window.emit("import", "import".to_string()).unwrap();
            }
            "quit" => {
                std::process::exit(0);
            }
            "close" => {
                event.window().close().unwrap();
            }
            _ => {}
        })
        .run(context)
        .expect("error while running tauri application");
}

上記のように修正する以下の画像のようなWindow Menuが設定される。

1枚目の画像のmenuは以下のコードで定義している。

    let mainmenu = Submenu::new(
        "Todo",
        Menu::new()
            .add_native_item(MenuItem::About(
                "todo".to_string(),
                AboutMetadata::default(),
            ))
            .add_native_item(MenuItem::Services)
            .add_native_item(MenuItem::Separator)
            .add_native_item(MenuItem::Services)
            .add_native_item(MenuItem::Separator)
            .add_native_item(MenuItem::Hide)
            .add_native_item(MenuItem::HideOthers)
            .add_native_item(MenuItem::ShowAll)
            .add_native_item(MenuItem::Separator)
            .add_native_item(MenuItem::Quit)
            .add_native_item(MenuItem::Separator)
            .add_item(about)
            .add_item(update)
            .add_native_item(MenuItem::Separator)
            .add_item(quit),
    );

add_native_itemで追加している項目は、Tauriがデフォルトで用意している項目で以下のような定義が存在している。

docs.rs

また、独自のWindow Menuを追加する場合は以下のように書く。 CustomMenuItem::newで項目を追加、acceleratorでショートカットを設定できる。

    let export = CustomMenuItem::new("export".to_string(), "Export").accelerator("Cmd+Shift+E");
    let import = CustomMenuItem::new("import".to_string(), "Import").accelerator("Cmd+Shift+I");

(略)
    let filemenu = Submenu::new("File", Menu::new().add_item(export).add_item(import));

Window Menuをクリックした時の動作は以下の部分に記載。

        .on_menu_event(|event| match event.menu_item_id() {
        
          (略)

            "export" => {
                let window = event.window();
                window.emit("export", "export".to_string()).unwrap();
            }
            "import" => {
                let window = event.window();
                window.emit("import", "import".to_string()).unwrap();
            }

          (略)

        })
        .run(context)

window.emitを実行する事でWebView側でアクションを送信することができる。送信したアクションTauri API経由で取得可能。詳しくは以下を参照。

tauri.app

なので、WebView側に監視用のHooksを以下のように追加。

src/hooks/useListen.tsx

  useEffect(() => {
    let unListenExport: any;
    let unListenImport: any;

    async function f() {
      unListenExport = await listen<string>("export", async () => {
        const m = localStorage.getItem(STORAGE_KEY.MARKDOWN) || "";
        const h = getJsonParse(STORAGE_KEY.HISTORY);
        const t = getJsonParse(STORAGE_KEY.TASK_LIST);
        const data = {
          markdown: m,
          history: h,
          tasks: t,
        };

        const path = await save({ defaultPath: "export-todo.json" });
        if (path) {
          writeTextFile(path, JSON.stringify(data));
        }
      });
      unListenImport = await listen<string>("import", async () => {
        const path = await open();
        if (path) {
          const dataText = await readTextFile(String(path));
          const data: any = JSON.parse(dataText);
          console.log(data);

          localStorage.setItem(STORAGE_KEY.MARKDOWN, data.markdown);
          localStorage.setItem(
            STORAGE_KEY.HISTORY,
            JSON.stringify(data.history)
          );
          localStorage.setItem(
            STORAGE_KEY.TASK_LIST,
            JSON.stringify(data.tasks)
          );

          props.onImportCallback(data.markdown, data.tasks, data.history);
        }
      });
    }
    f();

    return () => {
      if (unListenExport) {
        unListenExport();
      }
      if (unListenImport) {
        unListenImport();
      }
    };
  }, []);

これでWindow MenuからWebViewへの疎通部分は完了した。 最後にファイルのインポート/エクスポートに解説。

以下のTauri APIを使用することでファイルの選択と保存が可能。

tauri.app

tauri.app

コード的には以下の部分になる。

■ 読み込みのダイアログ表示 & ファイルの読み込み処理

import { save, open } from "@tauri-apps/api/dialog";
import { writeTextFile, readTextFile } from "@tauri-apps/api/fs";

const path = await open();
if (path) {
     const dataText = await readTextFile(String(path));

■ 読み込みのダイアログ表示 & ファイルの読み込み処理

import { save, open } from "@tauri-apps/api/dialog";
import { writeTextFile, readTextFile } from "@tauri-apps/api/fs";

const path = await open();
if (path) {
     const dataText = await readTextFile(String(path));

■ ファイルの読み込みのダイアログ表示 & ファイルの読み込み処理

import { save, open } from "@tauri-apps/api/dialog";
import { writeTextFile, readTextFile } from "@tauri-apps/api/fs";

const path = await open();
if (path) {
     const dataText = await readTextFile(String(path));

■ ファイルの保存のダイアログ表示 & ファイルの書き込み処理

import { save, open } from "@tauri-apps/api/dialog";
import { writeTextFile, readTextFile } from "@tauri-apps/api/fs";

const path = await save({ defaultPath: "export-todo.json" });
if (path) {
   writeTextFile(path, JSON.stringify(data));
 }

これでファイルの読み書きが可能になった。後は保存データをインポート/エクスポートしたい形式にフォーマットすればOK。

まとめ

Window Menuはwebやスマホアプリ開発時には無い概念だったので結構新鮮で開発していて発見が多かった。