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",
"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",

View File

@ -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",

View File

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

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 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<Mutex<Config>> = Lazy::new(|| Mutex::new(Config::load().unwrap()));
static DICTIONARY: Lazy<Mutex<Dictionary>> = Lazy::new(|| Mutex::new(Dictionary::load().unwrap()));
static APP_HANDLE: OnceLock<AppHandle> = OnceLock::new();
#[tauri::command]
fn load_settings(state: State<AppState>) -> Result<Config, String> {
@ -102,6 +104,29 @@ fn open_ms_settings_regionlanguage_jpnime() -> Result<(), String> {
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() {
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();

View File

@ -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<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) => {
// ブラウザの標準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 (
<>
<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 (
<div className="h-full">
<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="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">
v1.10.0
v1.11.0
</div>
</div>
@ -30,7 +209,7 @@ const AboutComponent: React.FC = () => {
<div className="text-sm">
<div className="flex items-center mb-1">
<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 className="flex items-center mb-1">
<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" />
</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>
{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>
{/* 更新ダイアログ */}
<UpdateDialog />
</div>
);
};

View File

@ -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";
@ -27,6 +29,13 @@ const AppContent = () => {
const [_isTsfAvailable, setIsTsfAvailable] = useState<boolean | null>(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<number>(0);
const [updateInfo, setUpdateInfo] = useState<{version: string, date: string | undefined, body: string | undefined} | null>(null);
useEffect(() => {
const unlisten = listen<Log>('addLog', (event) => {
setLogs(prevLogs => [{ time: event.payload.time, original: event.payload.original, converted: event.payload.converted }, ...prevLogs]);
@ -61,6 +70,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) => {
try {
@ -71,6 +107,51 @@ const AppContent = () => {
}
};
// 更新処理
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) => (
<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 className="text-xs text-gray-500 dark:text-gray-400 mb-1">{log.time}</div>
@ -187,6 +268,63 @@ const AppContent = () => {
</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>
);
};