diff --git a/package-lock.json b/package-lock.json index de89143..cd71fde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.11.0", "dependencies": { "@tauri-apps/api": "^2.0.0-rc.0", + "@tauri-apps/plugin-process": "^2.2.0", "@tauri-apps/plugin-shell": "^2.0.0", "@tauri-apps/plugin-updater": "^2.6.0", "react": "^18.2.0", @@ -1267,6 +1268,15 @@ "node": ">= 10" } }, + "node_modules/@tauri-apps/plugin-process": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-process/-/plugin-process-2.2.0.tgz", + "integrity": "sha512-uypN2Crmyop9z+KRJr3zl71OyVFgTuvHFjsJ0UxxQ/J5212jVa5w4nPEYjIewcn8bUEXacRebwE6F7owgrbhSw==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.0.0" + } + }, "node_modules/@tauri-apps/plugin-shell": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0.tgz", diff --git a/package.json b/package.json index 26fa903..68bf2c4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vrclipboard-ime-gui", "private": true, - "version": "1.11.0", + "version": "1.11.1", "type": "module", "scripts": { "dev": "vite", @@ -11,6 +11,7 @@ }, "dependencies": { "@tauri-apps/api": "^2.0.0-rc.0", + "@tauri-apps/plugin-process": "^2.2.0", "@tauri-apps/plugin-shell": "^2.0.0", "@tauri-apps/plugin-updater": "^2.6.0", "react": "^18.2.0", diff --git a/release.json b/release.json index c2da0cf..03162ca 100644 --- a/release.json +++ b/release.json @@ -1,7 +1,7 @@ { - "url": "https://r2-vrime.mii.dev/releases/vrclipboard-ime-gui_1.11.0_x64_ja-JP.msi.zip", - "version": "1.11.0", - "notes": "UIの変更, TSF再変換機能の正式リリース", + "url": "https://r2-vrime.mii.dev/releases/vrclipboard-ime-gui_1.11.1_x64_ja-JP.msi.zip", + "version": "1.11.1", + "notes": "UIの変更, TSF再変換機能の正式リリース, アップデート機能の修正", "pub_date": "2025-03-11T19:14:02+09:00", - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTTStkVlpUR0NpcVcvYitzcmhYakFkOTBWZVo5RHFwVk9xbjdzNlg3VHMrZ3NBaHpxS2VacEg4ckE3V3ZZSGZETzFyZE1Qc0JoRTltUGhvREdsUzF2Ry91c2RtWnlqOXdrPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzQxNjg4MzA3CWZpbGU6dnJjbGlwYm9hcmQtaW1lLWd1aV8xLjExLjBfeDY0X2phLUpQLm1zaS56aXAKVWt0b0t5YkR5MlczM1FRYXV2THRHYjQwWVFxRGR2OW1KTExJNGFpeGcyN2k2Q1hrU3Yrb3dxU3NvY2FLNTk4bDd1Q1lFanpaQktIM0UxdTBzNmd3Q3c9PQo=" + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTTStkVlpUR0NpcVlBbURsaEpHUmtuS3N5dUJzM0RaMnBCK2F4K1J5dFJYR0NoaWlvRUtJK203NEd6aDFXZjE5MFF6MUZqZDBwd0pVdWFpOW14ZDJaTDJKL0d4dGd0K1FrPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzQxNjk2MTEwCWZpbGU6dnJjbGlwYm9hcmQtaW1lLWd1aV8xLjExLjFfeDY0X2phLUpQLm1zaS56aXAKTmljczR2U0ZmSlZwbUZ2eEFuSmNUZEJ0L0ZHeSs0SmlRUjRRMU95OWpLTWZ5QktLSWRZdDgvMkpEV2hDQ00wSytWYm9WSHhpQm5naWlUQTBhQkhKRGc9PQo=" } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json new file mode 100644 index 0000000..12c67a6 --- /dev/null +++ b/src-tauri/capabilities/default.json @@ -0,0 +1,15 @@ +{ + "identifier": "desktop-capability", + "platforms": [ + "macOS", + "windows", + "linux" + ], + "windows": ["main"], + "permissions": [ + "updater:allow-check", + "updater:allow-download", + "updater:allow-install", + "updater:allow-download-and-install" + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/desktop.json b/src-tauri/capabilities/desktop.json deleted file mode 100644 index 9874144..0000000 --- a/src-tauri/capabilities/desktop.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "identifier": "desktop-capability", - "platforms": [ - "macOS", - "windows", - "linux" - ], - "permissions": [ - "updater:default", - "updater:default" - ] -} \ No newline at end of file diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ee7799a..edb25c3 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -14,20 +14,21 @@ mod tauri_emit_subscriber; mod tsf_availability; mod dictionary; -use std::sync::Mutex; +use std::sync::{Mutex, OnceLock}; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; -use tauri::{Manager, State}; +use tauri::{AppHandle, Manager, State}; use clipboard_master::Master; use com::Com; use config::Config; use handler::ConversionHandler; use tauri_emit_subscriber::TauriEmitSubscriber; +use tauri_plugin_updater::UpdaterExt; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use tsf_availability::check_tsf_availability; -use tracing::debug; +use tracing::{debug, error}; use dictionary::Dictionary; #[derive(Serialize, Deserialize, Debug, Clone)] @@ -44,6 +45,7 @@ struct AppState { static STATE: Lazy> = Lazy::new(|| Mutex::new(Config::load().unwrap())); static DICTIONARY: Lazy> = Lazy::new(|| Mutex::new(Dictionary::load().unwrap())); +static APP_HANDLE: OnceLock = OnceLock::new(); #[tauri::command] fn load_settings(state: State) -> Result { @@ -102,6 +104,29 @@ fn open_ms_settings_regionlanguage_jpnime() -> Result<(), String> { Ok(()) } +#[tauri::command] +async fn check_update() -> Result { + if let Some(app_handle) = APP_HANDLE.get() { + match app_handle.updater() { + Ok(updater) => match updater.check().await { + Ok(Some(_)) => Ok(true), + Ok(None) => Ok(false), + Err(e) => { + error!("Failed to check for updates: {}", e); + Err(format!("Failed to check for updates: {}", e)) + }, + }, + Err(e) => { + error!("Updater not available: {}", e); + Err("Updater not available".to_string()) + } + } + } else { + error!("App handle not set"); + Err("App handle not set".to_string()) + } +} + fn main() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) @@ -116,8 +141,18 @@ fn main() { Dictionary::load().expect("Failed to load default dictionary") })), }) - .invoke_handler(tauri::generate_handler![load_settings, save_settings, check_tsf_availability_command, open_ms_settings_regionlanguage_jpnime, load_dictionary, save_dictionary]) + .invoke_handler(tauri::generate_handler![ + load_settings, + save_settings, + check_tsf_availability_command, + open_ms_settings_regionlanguage_jpnime, + load_dictionary, + save_dictionary, + check_update + ]) .setup(|app| { + APP_HANDLE.set(app.app_handle().to_owned()).unwrap(); + let _span = tracing::span!(tracing::Level::INFO, "main"); app.manage(STATE.lock().unwrap().clone()); app.manage(DICTIONARY.lock().unwrap().clone()); @@ -128,6 +163,15 @@ fn main() { }); registry.init(); + let update_handle = app_handle.clone(); + tauri::async_runtime::spawn(async move { + let _ = update_handle.updater() + .unwrap() + .check() + .await + .map_err(|e| tracing::error!("Failed to check for updates: {}", e)); + }); + std::thread::spawn(move || { let _com = Com::new().unwrap(); @@ -142,4 +186,4 @@ fn main() { }) .run(tauri::generate_context!()) .expect("error while running tauri application"); -} +} \ No newline at end of file diff --git a/src/AboutComponent.tsx b/src/AboutComponent.tsx index 652c622..3d98812 100644 --- a/src/AboutComponent.tsx +++ b/src/AboutComponent.tsx @@ -1,12 +1,191 @@ -import React from 'react'; -import { Github, ExternalLink, Coffee } from 'lucide-react'; +import React, { useState } from 'react'; +import { Github, ExternalLink, Coffee, RefreshCw, Check, AlertCircle, Download, X } from 'lucide-react'; +import { check } from '@tauri-apps/plugin-updater'; +import { relaunch } from '@tauri-apps/plugin-process'; const AboutComponent: React.FC = () => { + const [updateStatus, setUpdateStatus] = useState<'idle' | 'checking' | 'available' | 'downloading' | 'notAvailable' | 'error'>('idle'); + const [downloadProgress, setDownloadProgress] = useState(0); + const [contentLength, setContentLength] = useState(0); + const [updateInfo, setUpdateInfo] = useState<{version: string, date: string | undefined, body: string | undefined} | null>(null); + const [showUpdateDialog, setShowUpdateDialog] = useState(false); + const openLink = (url: string) => { - // ブラウザの標準APIを使用してリンクを開く window.open(url, '_blank', 'noopener,noreferrer'); }; + const checkForUpdates = async () => { + try { + setUpdateStatus('checking'); + const update = await check(); + + if (update) { + console.log(`更新が見つかりました: ${update.version} (${update.date}) - ${update.body}`); + setUpdateStatus('available'); + setUpdateInfo({ + version: update.version, + date: update.date, + body: update.body + }); + setShowUpdateDialog(true); + } else { + setUpdateStatus('notAvailable'); + setTimeout(() => setUpdateStatus('idle'), 3000); + } + } catch (error) { + console.error('Update check error:', error); + setUpdateStatus('error'); + setTimeout(() => setUpdateStatus('idle'), 3000); + } + }; + + const handleInstallUpdate = async () => { + try { + setUpdateStatus('downloading'); + const update = await check(); + if (!update) { + setUpdateStatus('error'); + setTimeout(() => setUpdateStatus('idle'), 3000); + return; + } + + let downloaded = 0; + + // 更新をダウンロードしてインストール + await update.downloadAndInstall((event: any) => { + switch (event.event) { + case 'Started': + setContentLength(event.data.contentLength); + console.log(`ダウンロード開始: ${event.data.contentLength} bytes`); + break; + case 'Progress': + downloaded += event.data.chunkLength; + const progress = (downloaded / contentLength) * 100; + setDownloadProgress(progress); + console.log(`ダウンロード中: ${downloaded} / ${contentLength} (${progress.toFixed(1)}%)`); + break; + case 'Finished': + console.log('ダウンロード完了'); + break; + } + }); + + console.log('更新がインストールされました'); + // アプリを再起動 + await relaunch(); + } catch (error) { + console.error('Update installation error:', error); + setUpdateStatus('error'); + setTimeout(() => setUpdateStatus('idle'), 3000); + } + }; + + const getUpdateButtonContent = () => { + switch (updateStatus) { + case 'idle': + return ( + <> + + 更新を確認 + + ); + case 'checking': + return ( + <> + + 確認中... + + ); + case 'available': + return ( + <> + + 更新が見つかりました + + ); + case 'downloading': + return ( + <> + + ダウンロード中... {Math.round(downloadProgress)}% + + ); + case 'notAvailable': + return ( + <> + + 最新バージョンです + + ); + case 'error': + return ( + <> + + エラーが発生しました + + ); + } + }; + + // 更新ダイアログの表示 + const UpdateDialog = () => { + if (!showUpdateDialog || !updateInfo) return null; + + return ( +
+
+
+

+ アップデートが利用可能です +

+ +
+ +
+
+ バージョン: + {updateInfo.version} +
+
+ リリース日: + {updateInfo.date} +
+
+ 変更内容: +
+ {updateInfo.body} +
+
+
+ +
+ + +
+
+
+ ); + }; + return (

@@ -18,7 +197,7 @@ const AboutComponent: React.FC = () => {
VRClipboard-IME
- v1.10.0 + v1.11.0
@@ -30,7 +209,7 @@ const AboutComponent: React.FC = () => {
バージョン: - 1.11.0 + 1.11.1
ライセンス: @@ -92,10 +271,33 @@ const AboutComponent: React.FC = () => { ウェブサイト +
+ + {updateStatus === 'downloading' && ( +
+
+
+
+
+ )}

+ + {/* 更新ダイアログ */} + ); }; diff --git a/src/App.tsx b/src/App.tsx index 9329d6f..bdb08fa 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,9 @@ import { useState, useEffect } from "react"; -import { List, Settings, Terminal, Bug, Info, Check } from 'lucide-react'; +import { List, Settings, Terminal, Bug, Info, Check, Download, X } from 'lucide-react'; import { listen } from "@tauri-apps/api/event"; import { invoke } from "@tauri-apps/api/core"; +import { check } from '@tauri-apps/plugin-updater'; +import { relaunch } from '@tauri-apps/plugin-process'; import "./App.css"; import TitleBar from "./TitleBar"; import SettingsComponent from "./SettingsComponent"; @@ -26,6 +28,13 @@ const AppContent = () => { const [currentSettings, setCurrentSettings] = useState(null); const [_isTsfAvailable, setIsTsfAvailable] = useState(null); const [showTsfSuccessMessage, setShowTsfSuccessMessage] = useState(false); + + // 更新関連のstate + const [updateAvailable, setUpdateAvailable] = useState(false); + const [updateDownloading, setUpdateDownloading] = useState(false); + const [updateProgress, setUpdateProgress] = useState(0); + const [contentLength, setContentLength] = useState(0); + const [updateInfo, setUpdateInfo] = useState<{version: string, date: string | undefined, body: string | undefined} | null>(null); useEffect(() => { const unlisten = listen('addLog', (event) => { @@ -60,6 +69,33 @@ const AppContent = () => { checkTsfSettings(); }, []); + + // アプリ起動時に更新を自動チェック + useEffect(() => { + const checkForUpdates = async () => { + try { + const update = await check(); + if (update) { + console.log(`更新が見つかりました: ${update.version} (${update.date}) - ${update.body}`); + setUpdateAvailable(true); + setUpdateInfo({ + version: update.version, + date: update.date, + body: update.body + }); + } + } catch (error) { + console.error('更新チェックエラー:', error); + } + }; + + // アプリ起動から少し遅らせて更新チェック + const timer = setTimeout(() => { + checkForUpdates(); + }, 3000); + + return () => clearTimeout(timer); + }, []); // 設定保存関数 const saveSettings = async (config: Config) => { @@ -70,6 +106,51 @@ const AppContent = () => { console.error('設定保存エラー:', error); } }; + + // 更新処理 + const handleInstallUpdate = async () => { + try { + setUpdateDownloading(true); + setUpdateAvailable(false); + + const update = await check(); + if (!update) { + setUpdateDownloading(false); + return; + } + + let downloaded = 0; + + await update.downloadAndInstall((event: any) => { + switch (event.event) { + case 'Started': + setContentLength(event.data.contentLength); + console.log(`ダウンロード開始: ${event.data.contentLength} bytes`); + break; + case 'Progress': + downloaded += event.data.chunkLength; + const progress = contentLength > 0 ? (downloaded / contentLength) * 100 : 0; + setUpdateProgress(progress); + console.log(`ダウンロード中: ${downloaded} / ${contentLength} (${progress.toFixed(1)}%)`); + break; + case 'Finished': + console.log('ダウンロード完了'); + break; + } + }); + + console.log('更新がインストールされました'); + // アプリを再起動 + await relaunch(); + } catch (error) { + console.error('更新インストールエラー:', error); + setUpdateDownloading(false); + } + }; + + const dismissUpdate = () => { + setUpdateAvailable(false); + }; const renderLogEntry = (log: { time: string; original: string; converted: string }, index: number) => (
@@ -187,6 +268,63 @@ const AppContent = () => {
)} + + {/* 更新通知 */} + {updateAvailable && !updateDownloading && updateInfo && ( +
+
+

アップデートが利用可能です

+ +
+
+
+ バージョン: + {updateInfo.version} +
+
+ リリース日: + {updateInfo.date} +
+
+ 変更内容: +

+ {updateInfo.body} +

+
+
+
+ + +
+
+ )} + + {/* ダウンロード中ダイアログ */} + {updateDownloading && ( +
+

ダウンロード中...

+
+
+
+

{Math.round(updateProgress)}%

+
+ )}
); };