fix updater

This commit is contained in:
mii443
2025-03-11 21:44:29 +09:00
parent 185e1bcf73
commit 15a5d5c8cb
8 changed files with 426 additions and 28 deletions

10
package-lock.json generated
View File

@ -9,6 +9,7 @@
"version": "1.11.0", "version": "1.11.0",
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.0.0-rc.0", "@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-shell": "^2.0.0",
"@tauri-apps/plugin-updater": "^2.6.0", "@tauri-apps/plugin-updater": "^2.6.0",
"react": "^18.2.0", "react": "^18.2.0",
@ -1267,6 +1268,15 @@
"node": ">= 10" "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": { "node_modules/@tauri-apps/plugin-shell": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-shell/-/plugin-shell-2.0.0.tgz",

View File

@ -1,7 +1,7 @@
{ {
"name": "vrclipboard-ime-gui", "name": "vrclipboard-ime-gui",
"private": true, "private": true,
"version": "1.11.0", "version": "1.11.1",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@ -11,6 +11,7 @@
}, },
"dependencies": { "dependencies": {
"@tauri-apps/api": "^2.0.0-rc.0", "@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-shell": "^2.0.0",
"@tauri-apps/plugin-updater": "^2.6.0", "@tauri-apps/plugin-updater": "^2.6.0",
"react": "^18.2.0", "react": "^18.2.0",

View File

@ -1,7 +1,7 @@
{ {
"url": "https://r2-vrime.mii.dev/releases/vrclipboard-ime-gui_1.11.0_x64_ja-JP.msi.zip", "url": "https://r2-vrime.mii.dev/releases/vrclipboard-ime-gui_1.11.1_x64_ja-JP.msi.zip",
"version": "1.11.0", "version": "1.11.1",
"notes": "UIの変更, TSF再変換機能の正式リリース", "notes": "UIの変更, TSF再変換機能の正式リリース, アップデート機能の修正",
"pub_date": "2025-03-11T19:14:02+09:00", "pub_date": "2025-03-11T19:14:02+09:00",
"signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTTStkVlpUR0NpcVcvYitzcmhYakFkOTBWZVo5RHFwVk9xbjdzNlg3VHMrZ3NBaHpxS2VacEg4ckE3V3ZZSGZETzFyZE1Qc0JoRTltUGhvREdsUzF2Ry91c2RtWnlqOXdrPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzQxNjg4MzA3CWZpbGU6dnJjbGlwYm9hcmQtaW1lLWd1aV8xLjExLjBfeDY0X2phLUpQLm1zaS56aXAKVWt0b0t5YkR5MlczM1FRYXV2THRHYjQwWVFxRGR2OW1KTExJNGFpeGcyN2k2Q1hrU3Yrb3dxU3NvY2FLNTk4bDd1Q1lFanpaQktIM0UxdTBzNmd3Q3c9PQo=" "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTTStkVlpUR0NpcVlBbURsaEpHUmtuS3N5dUJzM0RaMnBCK2F4K1J5dFJYR0NoaWlvRUtJK203NEd6aDFXZjE5MFF6MUZqZDBwd0pVdWFpOW14ZDJaTDJKL0d4dGd0K1FrPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzQxNjk2MTEwCWZpbGU6dnJjbGlwYm9hcmQtaW1lLWd1aV8xLjExLjFfeDY0X2phLUpQLm1zaS56aXAKTmljczR2U0ZmSlZwbUZ2eEFuSmNUZEJ0L0ZHeSs0SmlRUjRRMU95OWpLTWZ5QktLSWRZdDgvMkpEV2hDQ00wSytWYm9WSHhpQm5naWlUQTBhQkhKRGc9PQo="
} }

View File

@ -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"
]
}

View File

@ -1,12 +0,0 @@
{
"identifier": "desktop-capability",
"platforms": [
"macOS",
"windows",
"linux"
],
"permissions": [
"updater:default",
"updater:default"
]
}

View File

@ -14,20 +14,21 @@ mod tauri_emit_subscriber;
mod tsf_availability; mod tsf_availability;
mod dictionary; mod dictionary;
use std::sync::Mutex; use std::sync::{Mutex, OnceLock};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tauri::{Manager, State}; use tauri::{AppHandle, Manager, State};
use clipboard_master::Master; use clipboard_master::Master;
use com::Com; use com::Com;
use config::Config; use config::Config;
use handler::ConversionHandler; use handler::ConversionHandler;
use tauri_emit_subscriber::TauriEmitSubscriber; use tauri_emit_subscriber::TauriEmitSubscriber;
use tauri_plugin_updater::UpdaterExt;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use tsf_availability::check_tsf_availability; use tsf_availability::check_tsf_availability;
use tracing::debug; use tracing::{debug, error};
use dictionary::Dictionary; use dictionary::Dictionary;
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
@ -44,6 +45,7 @@ struct AppState {
static STATE: Lazy<Mutex<Config>> = Lazy::new(|| Mutex::new(Config::load().unwrap())); static STATE: Lazy<Mutex<Config>> = Lazy::new(|| Mutex::new(Config::load().unwrap()));
static DICTIONARY: Lazy<Mutex<Dictionary>> = Lazy::new(|| Mutex::new(Dictionary::load().unwrap())); static DICTIONARY: Lazy<Mutex<Dictionary>> = Lazy::new(|| Mutex::new(Dictionary::load().unwrap()));
static APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();
#[tauri::command] #[tauri::command]
fn load_settings(state: State<AppState>) -> Result<Config, String> { fn load_settings(state: State<AppState>) -> Result<Config, String> {
@ -102,6 +104,29 @@ fn open_ms_settings_regionlanguage_jpnime() -> Result<(), String> {
Ok(()) Ok(())
} }
#[tauri::command]
async fn check_update() -> Result<bool, String> {
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() { fn main() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
@ -116,8 +141,18 @@ fn main() {
Dictionary::load().expect("Failed to load default dictionary") 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| { .setup(|app| {
APP_HANDLE.set(app.app_handle().to_owned()).unwrap();
let _span = tracing::span!(tracing::Level::INFO, "main"); let _span = tracing::span!(tracing::Level::INFO, "main");
app.manage(STATE.lock().unwrap().clone()); app.manage(STATE.lock().unwrap().clone());
app.manage(DICTIONARY.lock().unwrap().clone()); app.manage(DICTIONARY.lock().unwrap().clone());
@ -128,6 +163,15 @@ fn main() {
}); });
registry.init(); 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 || { std::thread::spawn(move || {
let _com = Com::new().unwrap(); let _com = Com::new().unwrap();
@ -142,4 +186,4 @@ fn main() {
}) })
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@ -1,12 +1,191 @@
import React from 'react'; import React, { useState } from 'react';
import { Github, ExternalLink, Coffee } from 'lucide-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 AboutComponent: React.FC = () => {
const [updateStatus, setUpdateStatus] = useState<'idle' | 'checking' | 'available' | 'downloading' | 'notAvailable' | 'error'>('idle');
const [downloadProgress, setDownloadProgress] = useState<number>(0);
const [contentLength, setContentLength] = useState<number>(0);
const [updateInfo, setUpdateInfo] = useState<{version: string, date: string | undefined, body: string | undefined} | null>(null);
const [showUpdateDialog, setShowUpdateDialog] = useState(false);
const openLink = (url: string) => { const openLink = (url: string) => {
// ブラウザの標準APIを使用してリンクを開く
window.open(url, '_blank', 'noopener,noreferrer'); 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 (
<>
<RefreshCw size={14} className="mr-1.5" />
</>
);
case 'checking':
return (
<>
<RefreshCw size={14} className="mr-1.5 animate-spin" />
...
</>
);
case 'available':
return (
<>
<Download size={14} className="mr-1.5" />
</>
);
case 'downloading':
return (
<>
<Download size={14} className="mr-1.5 animate-pulse" />
... {Math.round(downloadProgress)}%
</>
);
case 'notAvailable':
return (
<>
<Check size={14} className="mr-1.5" />
</>
);
case 'error':
return (
<>
<AlertCircle size={14} className="mr-1.5" />
</>
);
}
};
// 更新ダイアログの表示
const UpdateDialog = () => {
if (!showUpdateDialog || !updateInfo) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-5 max-w-md w-full">
<div className="flex justify-between items-start mb-4">
<h3 className="text-lg font-medium text-gray-800 dark:text-gray-200">
</h3>
<button
onClick={() => setShowUpdateDialog(false)}
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
<X size={18} />
</button>
</div>
<div className="mb-4">
<div className="flex items-center mb-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 w-20">:</span>
<span className="text-sm text-gray-600 dark:text-gray-400">{updateInfo.version}</span>
</div>
<div className="flex items-center mb-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 w-20">:</span>
<span className="text-sm text-gray-600 dark:text-gray-400">{updateInfo.date}</span>
</div>
<div className="mb-2">
<span className="text-sm font-medium text-gray-700 dark:text-gray-300 block mb-1">:</span>
<div className="text-sm text-gray-600 dark:text-gray-400 bg-gray-50 dark:bg-gray-700 p-2 rounded border border-gray-200 dark:border-gray-600">
{updateInfo.body}
</div>
</div>
</div>
<div className="flex justify-end space-x-3">
<button
onClick={() => setShowUpdateDialog(false)}
className="px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded hover:bg-gray-300 dark:hover:bg-gray-600 text-sm"
>
</button>
<button
onClick={() => {
setShowUpdateDialog(false);
handleInstallUpdate();
}}
className="px-4 py-2 bg-indigo-500 text-white rounded hover:bg-indigo-600 text-sm flex items-center"
>
<Download size={14} className="mr-1.5" />
</button>
</div>
</div>
</div>
);
};
return ( return (
<div className="h-full"> <div className="h-full">
<h2 className="text-base font-medium mb-4 text-gray-700 dark:text-gray-200 flex items-center transition-colors"> <h2 className="text-base font-medium mb-4 text-gray-700 dark:text-gray-200 flex items-center transition-colors">
@ -18,7 +197,7 @@ const AboutComponent: React.FC = () => {
<div className="flex flex-col sm:flex-row items-start sm:items-center mb-4"> <div className="flex flex-col sm:flex-row items-start sm:items-center mb-4">
<div className="font-semibold text-lg text-indigo-600 dark:text-indigo-400 mr-3">VRClipboard-IME</div> <div className="font-semibold text-lg text-indigo-600 dark:text-indigo-400 mr-3">VRClipboard-IME</div>
<div className="bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 text-xs py-1 px-2 rounded"> <div className="bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 text-xs py-1 px-2 rounded">
v1.10.0 v1.11.0
</div> </div>
</div> </div>
@ -30,7 +209,7 @@ const AboutComponent: React.FC = () => {
<div className="text-sm"> <div className="text-sm">
<div className="flex items-center mb-1"> <div className="flex items-center mb-1">
<span className="text-gray-700 dark:text-gray-300 w-20">:</span> <span className="text-gray-700 dark:text-gray-300 w-20">:</span>
<span className="text-gray-600 dark:text-gray-400">1.11.0</span> <span className="text-gray-600 dark:text-gray-400">1.11.1</span>
</div> </div>
<div className="flex items-center mb-1"> <div className="flex items-center mb-1">
<span className="text-gray-700 dark:text-gray-300 w-20">:</span> <span className="text-gray-700 dark:text-gray-300 w-20">:</span>
@ -92,10 +271,33 @@ const AboutComponent: React.FC = () => {
<ExternalLink size={14} className="mr-1.5" /> <ExternalLink size={14} className="mr-1.5" />
</button> </button>
<button
onClick={checkForUpdates}
disabled={updateStatus === 'checking' || updateStatus === 'downloading'}
className={`flex items-center text-gray-600 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 px-3 py-1.5 rounded text-sm ${
(updateStatus === 'checking' || updateStatus === 'downloading') ? 'opacity-70 cursor-not-allowed' : ''
}`}
>
{getUpdateButtonContent()}
</button>
</div> </div>
</div> </div>
{updateStatus === 'downloading' && (
<div className="mt-2">
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded h-2 overflow-hidden">
<div
className="bg-indigo-500 h-full rounded transition-all duration-300"
style={{ width: `${downloadProgress}%` }}
/>
</div>
</div>
)}
</div> </div>
</div> </div>
{/* 更新ダイアログ */}
<UpdateDialog />
</div> </div>
); );
}; };

View File

@ -1,7 +1,9 @@
import { useState, useEffect } from "react"; 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 { listen } from "@tauri-apps/api/event";
import { invoke } from "@tauri-apps/api/core"; 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 "./App.css";
import TitleBar from "./TitleBar"; import TitleBar from "./TitleBar";
import SettingsComponent from "./SettingsComponent"; import SettingsComponent from "./SettingsComponent";
@ -26,6 +28,13 @@ const AppContent = () => {
const [currentSettings, setCurrentSettings] = useState<Config | null>(null); const [currentSettings, setCurrentSettings] = useState<Config | null>(null);
const [_isTsfAvailable, setIsTsfAvailable] = useState<boolean | null>(null); const [_isTsfAvailable, setIsTsfAvailable] = useState<boolean | null>(null);
const [showTsfSuccessMessage, setShowTsfSuccessMessage] = useState(false); 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<number>(0);
const [updateInfo, setUpdateInfo] = useState<{version: string, date: string | undefined, body: string | undefined} | null>(null);
useEffect(() => { useEffect(() => {
const unlisten = listen<Log>('addLog', (event) => { const unlisten = listen<Log>('addLog', (event) => {
@ -60,6 +69,33 @@ const AppContent = () => {
checkTsfSettings(); 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) => { const saveSettings = async (config: Config) => {
@ -70,6 +106,51 @@ const AppContent = () => {
console.error('設定保存エラー:', error); 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) => ( const renderLogEntry = (log: { time: string; original: string; converted: string }, index: number) => (
<div key={index} className="mb-2 p-2 bg-white/90 dark:bg-gray-800 rounded border border-gray-100 dark:border-gray-700 text-sm transition-colors"> <div key={index} className="mb-2 p-2 bg-white/90 dark:bg-gray-800 rounded border border-gray-100 dark:border-gray-700 text-sm transition-colors">
@ -187,6 +268,63 @@ const AppContent = () => {
</div> </div>
</div> </div>
)} )}
{/* 更新通知 */}
{updateAvailable && !updateDownloading && updateInfo && (
<div className="fixed bottom-4 right-4 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 p-4 rounded shadow-lg flex flex-col max-w-xs z-50">
<div className="flex justify-between items-start mb-2">
<h3 className="text-sm font-medium text-gray-800 dark:text-gray-200"></h3>
<button onClick={dismissUpdate} className="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200">
<X size={16} />
</button>
</div>
<div className="text-xs text-gray-600 dark:text-gray-400 mb-3">
<div className="mb-1">
<span className="font-medium">: </span>
{updateInfo.version}
</div>
<div className="mb-2">
<span className="font-medium">: </span>
{updateInfo.date}
</div>
<div>
<span className="font-medium">: </span>
<p className="mt-1 bg-gray-50 dark:bg-gray-700 p-2 rounded border border-gray-200 dark:border-gray-600">
{updateInfo.body}
</p>
</div>
</div>
<div className="flex space-x-2">
<button
onClick={dismissUpdate}
className="flex-1 text-gray-600 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 py-1.5 rounded text-sm"
>
</button>
<button
onClick={handleInstallUpdate}
className="flex-1 bg-indigo-500 hover:bg-indigo-600 text-white py-1.5 rounded text-sm flex items-center justify-center"
>
<Download size={14} className="mr-1.5" />
</button>
</div>
</div>
)}
{/* ダウンロード中ダイアログ */}
{updateDownloading && (
<div className="fixed bottom-4 right-4 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 p-4 rounded shadow-lg flex flex-col max-w-xs z-50">
<h3 className="text-sm font-medium text-gray-800 dark:text-gray-200 mb-2">...</h3>
<div className="w-full bg-gray-200 dark:bg-gray-700 rounded h-2 overflow-hidden">
<div
className="bg-indigo-500 h-full rounded transition-all duration-300"
style={{ width: `${updateProgress}%` }}
/>
</div>
<p className="text-xs text-gray-600 dark:text-gray-400 mt-1 text-right">{Math.round(updateProgress)}%</p>
</div>
)}
</div> </div>
); );
}; };