[Tauri]クリップボードの文字を自動翻訳してデスクトップ通知で表示するアプリ

## はじめに
今回はTauriを使ってクリップボードの値を読み取って、自動翻訳しデスクトップ通知として表示するアプリを作成したので紹介します。

https://github.com/taiyou1116/tauri-transition-helper

#### 通知
このように通知を表示できます。
![image_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2965124/c7f3a175-a1b0-fa94-f545-2d10cc75c198.png)


## 作る経緯
最近英語学習として英語のスクリプト付きの動画を見ているのですが、ちょくちょく分からない単語が出てきます。
別にその都度 `コピー -> 別ブラウザのGoogle翻訳を開く -> ペースト`としてもいいのですが、この作業が意外と面倒なので同じ画面上に翻訳が出てくれるアプリを作ったら便利なのではと思い作成しました。

## 使用する外部ライブラリ
### フロント
- tailwindcss ... HTML内で直接スタイリングを作れる
- react-hot-toast ... 簡単にフロントに通知機能を実装できる
### バックエンド
- reqwest ... httpリクエストを簡単に送信できる
- tokio ... 非同期処理を行う
- dotenv ... 環境変数を.envからロードできる
- clipboard ... クリップボードを読み取りできる
- lazy_static ... スレッド間で安全な値を扱える

## コード解説
クリップボードの内容を取得する方法ですが、`clipboardクレート`を使用しています。
このクレートは`get_contents`メソッドを使用し現在のクリップボードの値を取得できます。
なので、1秒間隔でクリップボードの値を取得し、前回の値と違っていた場合にその後の翻訳処理をおこなっています。
`tokio::spawn`で`run関数`を新しいタスクで実行しているのは、この関数がloopしていて、処理が終わらない(LOOP_FLAGがfalseになるまで)ためです。
また、　新しくタスクを生成し非同期関数をそのタスク内で行う場合、`.await`をまたいでSendトレイトを実装していない値は存在できません。そのため、`ctx`や`flag`はその前にドロップするようにしています。

```rust:commands.rs
use crate::{config, transition};
use clipboard::{ClipboardContext, ClipboardProvider};
use lazy_static::lazy_static;
use reqwest;
use std::sync::Mutex;
use std::time::Duration;
use tauri::Window;

pub const BUNDLE_IDENTIFIER: &str = "com.taiyou.tauri-transition-helper";

// loop処理をしているかは `LOOP_FLAG` で管理
lazy_static! {
    static ref LOOP_FLAG: Mutex<bool> = Mutex::new(false);
}

#[tauri::command]
pub async fn start_monitor_from_flont(window: Window) {
    let _join = tokio::spawn(async move {
        run(window).await;
    });
}

#[tauri::command]
pub async fn stop_transition() {
    // lockで他のスレッドからは読み書きできないようにする
    let mut flag = LOOP_FLAG.lock().unwrap();
    *flag = true;
}

async fn run(window: Window) {
    let config_instance = config::Config::new().expect("Failed to load config");
    let api_key = config_instance.api_key;

    let mut ctx: ClipboardContext = match ClipboardProvider::new() {
        Ok(context) => context,
        Err(e) => {
            eprintln!("{}", e);
            return;
        }
    };

    let mut last_clipboard_content = String::new();
    let mut contents = None;

    let client = reqwest::Client::new();

    loop {
        {
            let flag = LOOP_FLAG.lock().unwrap();
            if *flag == true {
                break;
            }
        }

        // クリップボードからテキストを取得
        contents = match ctx.get_contents() {
            Ok(ctx_contents) => Some(ctx_contents),
            Err(e) => {
                eprintln!("Error: {}", e);
                None // エラーが発生した場合はNoneを設定
            }
        };

        // `content` が Some(value) であり、`last_clipboard_content` と異なる場合
        if contents.as_ref() != Some(&last_clipboard_content) {
            if let Some(c) = contents {
                println!("New clipboard content: {}", c);
                last_clipboard_content = c;

                // ここで翻訳とデスクトップ通知を行う
                match transition::run(&api_key, &last_clipboard_content, &client).await {
                    Ok(translated_text) => {
                        // 通知
                        if let Err(e) = window.emit("issueNotification", Some(translated_text)) {
                            eprintln!("Failed to emit event: {}", e);
                        }
                    }
                    Err(e) => {
                        eprintln!("Error: {}", e.to_string());
                    }
                }
            }
        }

        // 1秒待機（ポーリング間隔）
        tokio::time::sleep(Duration::from_secs(1)).await
    }

    // ループが終了したらFLAGをfalseにリセット
    let mut flag = LOOP_FLAG.lock().unwrap();
    *flag = false;
}
```

## 実際にアプリを使う
### 時前準備
このアプリを使うには時前準備が必要です...。
翻訳には `Cloud Translate API` というAPIを使っているのですが、APIの使用にはAPIキーが必要なのでまずはそれを取得しなければいけません。
といっても、簡単なので以下のような手段で取得しましょう。
https://webloco.webolha.com/other/google-translation-api/

#### 料金に関して
このAPIは月々500,000文字まで翻訳が無料なので個人で使用するには全く問題ありません。
[料金詳細はこちらから](https://cloud.google.com/translate/pricing?hl=ja)

### アプリ取得方法

#### Mac
APIキーを取得したら、上記のGitHubページの`Release`から`dmgファイル`をダウンロードしてください。
#### Windows
少し面倒ですがGitHubリポジトリを`clone`してから、ローカルで`build`してください。


## おわりに
今回は、クリップボードを監視し取得した値を翻訳しデスクトップ通知で表示するアプリを作成しました。
もしも、`間違った内容`や`もっと良い書き方`などありましたら、コメントで教えてくださるととても助かります。