add tsf availability check

This commit is contained in:
mii
2025-03-05 02:52:00 +09:00
parent 723445786e
commit 89b688176b
7 changed files with 244 additions and 10 deletions

BIN
public/windows_settings.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -11,6 +11,7 @@ mod transform_rule;
mod tsf; mod tsf;
mod tsf_conversion; mod tsf_conversion;
mod tauri_emit_subscriber; mod tauri_emit_subscriber;
mod tsf_availability;
use std::sync::Mutex; use std::sync::Mutex;
@ -24,6 +25,8 @@ use config::Config;
use handler::ConversionHandler; use handler::ConversionHandler;
use tauri_emit_subscriber::TauriEmitSubscriber; use tauri_emit_subscriber::TauriEmitSubscriber;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use tsf_availability::check_tsf_availability;
use tracing::debug;
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
struct Log { struct Log {
@ -56,6 +59,27 @@ fn save_settings(config: Config, state: State<AppState>) -> Result<(), String> {
config.save(state) config.save(state)
} }
#[tauri::command]
fn check_tsf_availability_command() -> Result<bool, String> {
debug!("Checking TSF availability");
match check_tsf_availability() {
Ok(result) => {
debug!("TSF availability check result: {}", result);
Ok(result)
},
Err(e) => Err(format!("Failed to check TSF availability: {}", e)),
}
}
#[tauri::command]
fn open_ms_settings_regionlanguage_jpnime() -> Result<(), String> {
let _ = std::process::Command::new("cmd")
.args(&["/C", "start", "ms-settings:regionlanguage-jpnime"])
.output()
.map_err(|e| format!("Failed to open MS Settings: {}", e))?;
Ok(())
}
fn main() { fn main() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_shell::init())
@ -66,7 +90,7 @@ fn main() {
Config::load().expect("Failed to load default config") Config::load().expect("Failed to load default config")
})), })),
}) })
.invoke_handler(tauri::generate_handler![load_settings, save_settings]) .invoke_handler(tauri::generate_handler![load_settings, save_settings, check_tsf_availability_command, open_ms_settings_regionlanguage_jpnime])
.setup(|app| { .setup(|app| {
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());

View File

@ -52,6 +52,7 @@ where
timestamp: format!("{}-{}-{} {}:{}:{}", now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second()), timestamp: format!("{}-{}-{} {}:{}:{}", now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second()),
}; };
println!("[{} {}] {} {}", event.timestamp, event.level, event.module_path, event.message);
if self if self
.app_handle .app_handle
.emit( .emit(

View File

@ -0,0 +1,24 @@
use anyhow::Result;
use tracing::{error, info};
use crate::tsf::{search_candidate_provider::SearchCandidateProvider, set_thread_local_input_settings};
pub fn check_tsf_availability() -> Result<bool> {
info!("Checking TSF availability");
if let Err(e) = set_thread_local_input_settings(true) {
error!("Failed to set thread local input settings: {:?}", e);
return Ok(false);
}
match SearchCandidateProvider::create() {
Ok(_) => {
info!("TSF is available");
Ok(true)
}
Err(e) => {
error!("Failed to create SearchCandidateProvider: {:?}", e);
Ok(false)
}
}
}

View File

@ -1,6 +1,7 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { List, Settings, Terminal, Bug, Info } from 'lucide-react'; import { List, Settings, Terminal, Bug, Info } 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 "./App.css"; import "./App.css";
import TitleBar from "./TitleBar"; import TitleBar from "./TitleBar";
import SettingsComponent from "./SettingsComponent"; import SettingsComponent from "./SettingsComponent";
@ -8,6 +9,8 @@ import { ThemeProvider } from "./ThemeContext";
import { LogProvider } from "./LogContext"; import { LogProvider } from "./LogContext";
import TerminalComponent from "./TerminalComponent"; import TerminalComponent from "./TerminalComponent";
import AboutComponent from "./AboutComponent"; import AboutComponent from "./AboutComponent";
import TsfSettingsModal from "./TsfSettingsModal";
import { Config } from "./SettingsComponent";
interface Log { interface Log {
time: string; time: string;
@ -18,6 +21,9 @@ interface Log {
const AppContent = () => { const AppContent = () => {
const [logs, setLogs] = useState<Log[]>([]); const [logs, setLogs] = useState<Log[]>([]);
const [activeMenuItem, setActiveMenuItem] = useState('home'); const [activeMenuItem, setActiveMenuItem] = useState('home');
const [showTsfModal, setShowTsfModal] = useState(false);
const [currentSettings, setCurrentSettings] = useState<Config | null>(null);
const [isTsfAvailable, setIsTsfAvailable] = useState<boolean | null>(null);
useEffect(() => { useEffect(() => {
const unlisten = listen<Log>('addLog', (event) => { const unlisten = listen<Log>('addLog', (event) => {
@ -29,6 +35,40 @@ const AppContent = () => {
} }
}, []); }, []);
// TSF設定のチェック
useEffect(() => {
const checkTsfSettings = async () => {
try {
const settings: Config = await invoke('load_settings');
setCurrentSettings(settings);
if (settings.use_tsf_reconvert) {
// TSF設定が有効な場合、TSFが利用可能か確認
const available: boolean = await invoke('check_tsf_availability_command');
setIsTsfAvailable(available);
if (!available) {
setShowTsfModal(true);
}
}
} catch (error) {
console.error('TSF設定確認エラー:', error);
}
};
checkTsfSettings();
}, []);
// 設定保存関数
const saveSettings = async (config: Config) => {
try {
await invoke('save_settings', { config });
setCurrentSettings(config);
} catch (error) {
console.error('設定保存エラー:', error);
}
};
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">
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">{log.time}</div> <div className="text-xs text-gray-500 dark:text-gray-400 mb-1">{log.time}</div>
@ -79,7 +119,7 @@ const AppContent = () => {
</div> </div>
); );
case 'settings': case 'settings':
return <SettingsComponent />; return <SettingsComponent setShowTsfModal={setShowTsfModal} />;
case 'terminal': case 'terminal':
return <TerminalComponent />; return <TerminalComponent />;
case 'about': case 'about':
@ -111,6 +151,16 @@ const AppContent = () => {
{renderContent()} {renderContent()}
</div> </div>
</div> </div>
{/* TSF設定モーダル */}
{currentSettings && (
<TsfSettingsModal
isOpen={showTsfModal}
onClose={() => setShowTsfModal(false)}
onSaveSettings={saveSettings}
currentSettings={currentSettings}
/>
)}
</div> </div>
); );
}; };

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
import { ChevronDown, Settings, Save, AlertCircle, Check } from 'lucide-react'; import { ChevronDown, Settings, Save, AlertCircle, Check } from 'lucide-react';
interface Config { export interface Config {
prefix: string; prefix: string;
split: string; split: string;
command: string; command: string;
@ -11,9 +11,10 @@ interface Config {
skip_url: boolean; skip_url: boolean;
use_tsf_reconvert: boolean; use_tsf_reconvert: boolean;
skip_on_out_of_vrc: boolean; skip_on_out_of_vrc: boolean;
tsf_announce?: boolean;
} }
enum OnCopyMode { export enum OnCopyMode {
ReturnToClipboard = 'ReturnToClipboard', ReturnToClipboard = 'ReturnToClipboard',
ReturnToChatbox = 'ReturnToChatbox', ReturnToChatbox = 'ReturnToChatbox',
SendDirectly = 'SendDirectly' SendDirectly = 'SendDirectly'
@ -57,9 +58,10 @@ interface CheckboxFieldProps {
label: React.ReactNode; label: React.ReactNode;
checked: boolean; checked: boolean;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
disabled?: boolean;
} }
const CheckboxField: React.FC<CheckboxFieldProps> = ({ id, name, label, checked, onChange }) => ( const CheckboxField: React.FC<CheckboxFieldProps> = ({ id, name, label, checked, onChange, disabled }) => (
<div className="mb-2"> <div className="mb-2">
<div className="flex items-start"> <div className="flex items-start">
<div className="flex items-center h-4"> <div className="flex items-center h-4">
@ -69,11 +71,16 @@ const CheckboxField: React.FC<CheckboxFieldProps> = ({ id, name, label, checked,
name={name} name={name}
checked={checked} checked={checked}
onChange={onChange} onChange={onChange}
className="h-3.5 w-3.5 text-indigo-500 dark:text-indigo-400 border-gray-300 dark:border-gray-600 rounded dark:bg-gray-700 transition-colors" disabled={disabled}
className={`h-3.5 w-3.5 text-indigo-500 dark:text-indigo-400 border-gray-300 dark:border-gray-600 rounded dark:bg-gray-700 transition-colors ${
disabled ? 'opacity-50 cursor-not-allowed' : ''
}`}
/> />
</div> </div>
<div className="ml-2 text-xs"> <div className="ml-2 text-xs">
<label htmlFor={id} className="text-gray-700 dark:text-gray-300 transition-colors"> <label htmlFor={id} className={`text-gray-700 dark:text-gray-300 transition-colors ${
disabled ? 'opacity-50 cursor-not-allowed' : ''
}`}>
{label} {label}
</label> </label>
</div> </div>
@ -81,7 +88,11 @@ const CheckboxField: React.FC<CheckboxFieldProps> = ({ id, name, label, checked,
</div> </div>
); );
const SettingsComponent: React.FC = () => { interface SettingsComponentProps {
setShowTsfModal: (show: boolean) => void;
}
const SettingsComponent: React.FC<SettingsComponentProps> = ({ setShowTsfModal }) => {
const [settings, setSettings] = useState<Config>({ const [settings, setSettings] = useState<Config>({
prefix: ';', prefix: ';',
split: '/', split: '/',
@ -136,6 +147,30 @@ const SettingsComponent: React.FC = () => {
saveSettings(newSettings); saveSettings(newSettings);
}; };
const handleTsfChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
const isChecked = e.target.checked;
if (isChecked) {
try {
const available: boolean = await invoke('check_tsf_availability_command');
if (!available && setShowTsfModal) {
setShowTsfModal(true);
return;
}
} catch (error) {
return;
}
}
const { name, value, type } = e.target;
const newSettings = {
...settings,
[name]: type === 'checkbox' ? isChecked : value
};
setSettings(newSettings);
saveSettings(newSettings);
};
const handleSelectChange = (value: OnCopyMode) => { const handleSelectChange = (value: OnCopyMode) => {
const newSettings = { ...settings, on_copy_mode: value }; const newSettings = { ...settings, on_copy_mode: value };
setSettings(newSettings); setSettings(newSettings);
@ -193,6 +228,7 @@ const SettingsComponent: React.FC = () => {
label="区切り文字" label="区切り文字"
value={settings.split} value={settings.split}
onChange={handleChange} onChange={handleChange}
disabled={settings.use_tsf_reconvert}
description="複数の変換モードを使いたい場合の区切り文字" description="複数の変換モードを使いたい場合の区切り文字"
/> />
@ -201,6 +237,7 @@ const SettingsComponent: React.FC = () => {
label="モード変更文字" label="モード変更文字"
value={settings.command} value={settings.command}
onChange={handleChange} onChange={handleChange}
disabled={settings.use_tsf_reconvert}
description="変換モードを変更するための文字" description="変換モードを変更するための文字"
/> />
@ -210,6 +247,7 @@ const SettingsComponent: React.FC = () => {
label="無条件で変換" label="無条件で変換"
checked={settings.ignore_prefix} checked={settings.ignore_prefix}
onChange={handleChange} onChange={handleChange}
disabled={settings.use_tsf_reconvert}
/> />
<InputField <InputField
@ -217,7 +255,7 @@ const SettingsComponent: React.FC = () => {
label="開始文字" label="開始文字"
value={settings.prefix} value={settings.prefix}
onChange={handleChange} onChange={handleChange}
disabled={settings.ignore_prefix} disabled={settings.ignore_prefix || settings.use_tsf_reconvert}
description="変換を開始する文字(無条件で変換がオンの場合は無効)" description="変換を開始する文字(無条件で変換がオンの場合は無効)"
/> />
@ -282,7 +320,7 @@ const SettingsComponent: React.FC = () => {
</span> </span>
} }
checked={settings.use_tsf_reconvert} checked={settings.use_tsf_reconvert}
onChange={handleChange} onChange={handleTsfChange}
/> />
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 ml-5 transition-colors"> <p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 ml-5 transition-colors">
Windows10/11 Microsoft IME 使 Windows10/11 Microsoft IME 使

97
src/TsfSettingsModal.tsx Normal file
View File

@ -0,0 +1,97 @@
import React from 'react';
import { open } from '@tauri-apps/plugin-shell';
import { AlertCircle, Settings, Check, X, ExternalLink } from 'lucide-react';
import { Config } from './SettingsComponent';
import { invoke } from '@tauri-apps/api/core';
interface TsfSettingsModalProps {
isOpen: boolean;
onClose: () => void;
onSaveSettings: (config: Config) => Promise<void>;
currentSettings: Config;
}
const TsfSettingsModal: React.FC<TsfSettingsModalProps> = ({
isOpen,
onClose,
onSaveSettings,
currentSettings
}) => {
if (!isOpen) return null;
const openWindowsSettings = async () => {
invoke('open_ms_settings_regionlanguage_jpnime');
};
const useLegacyConverter = async () => {
try {
const newSettings = { ...currentSettings, use_tsf_reconvert: false };
await onSaveSettings(newSettings);
onClose();
} catch (error) {
console.error('設定保存に失敗しました:', error);
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-2xl w-full max-h-[90vh] overflow-y-auto transition-colors">
<div className="flex justify-between items-start mb-4">
<h2 className="text-lg font-medium text-gray-800 dark:text-gray-200 flex items-center">
<AlertCircle size={18} className="mr-2 text-yellow-500" />
Windowsの設定が必要です
</h2>
<button onClick={onClose} className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<X size={20} />
</button>
</div>
<div className="flex flex-col sm:flex-row gap-3 justify-start mb-4">
<button
onClick={openWindowsSettings}
className="flex items-center justify-center bg-indigo-500 hover:bg-indigo-600 text-white px-4 py-2 rounded"
>
<Settings size={16} className="mr-2" />
Windows設定を開く
</button>
</div>
<div className="text-gray-700 dark:text-gray-300 mb-4">
<div className="bg-gray-50 dark:bg-gray-700 p-4 rounded mb-4">
<h3 className="text-sm font-medium mb-2 flex items-center">
<Settings size={14} className="mr-1.5" />
</h3>
<ol className="list-decimal list-inside text-sm space-y-1.5 ml-1">
<li>Windows設定を開く</li>
<li></li>
<li> Microsoft IME 使</li>
</ol>
</div>
</div>
<div className="bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg mb-6 overflow-hidden">
<div className="p-2 bg-gray-50 dark:bg-gray-800 border-b border-gray-200 dark:border-gray-600 text-sm font-medium text-gray-700 dark:text-gray-300">
Windows設定画面
</div>
<div className="p-4 flex justify-center">
<img src="./windows_settings.png"/>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-3 justify-end">
<button
onClick={useLegacyConverter}
className="flex items-center justify-center bg-gray-200 hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 text-gray-800 dark:text-gray-200 px-4 py-2 rounded"
>
<Check size={16} className="mr-2" />
使
<span className="text-xs text-red-500 ml-1.5">()</span>
</button>
</div>
</div>
</div>
);
};
export default TsfSettingsModal;