mirror of
https://github.com/mii443/vrclipboard-ime-gui.git
synced 2025-08-22 08:05:32 +00:00
add tsf availability check
This commit is contained in:
BIN
public/windows_settings.png
Normal file
BIN
public/windows_settings.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 53 KiB |
@ -11,6 +11,7 @@ mod transform_rule;
|
||||
mod tsf;
|
||||
mod tsf_conversion;
|
||||
mod tauri_emit_subscriber;
|
||||
mod tsf_availability;
|
||||
|
||||
use std::sync::Mutex;
|
||||
|
||||
@ -24,6 +25,8 @@ use config::Config;
|
||||
use handler::ConversionHandler;
|
||||
use tauri_emit_subscriber::TauriEmitSubscriber;
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
use tsf_availability::check_tsf_availability;
|
||||
use tracing::debug;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
struct Log {
|
||||
@ -56,6 +59,27 @@ fn save_settings(config: Config, state: State<AppState>) -> Result<(), String> {
|
||||
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() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
@ -66,7 +90,7 @@ fn main() {
|
||||
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| {
|
||||
let _span = tracing::span!(tracing::Level::INFO, "main");
|
||||
app.manage(STATE.lock().unwrap().clone());
|
||||
|
@ -52,6 +52,7 @@ where
|
||||
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
|
||||
.app_handle
|
||||
.emit(
|
||||
|
24
src-tauri/src/tsf_availability.rs
Normal file
24
src-tauri/src/tsf_availability.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
52
src/App.tsx
52
src/App.tsx
@ -1,6 +1,7 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { List, Settings, Terminal, Bug, Info } from 'lucide-react';
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import "./App.css";
|
||||
import TitleBar from "./TitleBar";
|
||||
import SettingsComponent from "./SettingsComponent";
|
||||
@ -8,6 +9,8 @@ import { ThemeProvider } from "./ThemeContext";
|
||||
import { LogProvider } from "./LogContext";
|
||||
import TerminalComponent from "./TerminalComponent";
|
||||
import AboutComponent from "./AboutComponent";
|
||||
import TsfSettingsModal from "./TsfSettingsModal";
|
||||
import { Config } from "./SettingsComponent";
|
||||
|
||||
interface Log {
|
||||
time: string;
|
||||
@ -18,6 +21,9 @@ interface Log {
|
||||
const AppContent = () => {
|
||||
const [logs, setLogs] = useState<Log[]>([]);
|
||||
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(() => {
|
||||
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) => (
|
||||
<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>
|
||||
@ -79,7 +119,7 @@ const AppContent = () => {
|
||||
</div>
|
||||
);
|
||||
case 'settings':
|
||||
return <SettingsComponent />;
|
||||
return <SettingsComponent setShowTsfModal={setShowTsfModal} />;
|
||||
case 'terminal':
|
||||
return <TerminalComponent />;
|
||||
case 'about':
|
||||
@ -111,6 +151,16 @@ const AppContent = () => {
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TSF設定モーダル */}
|
||||
{currentSettings && (
|
||||
<TsfSettingsModal
|
||||
isOpen={showTsfModal}
|
||||
onClose={() => setShowTsfModal(false)}
|
||||
onSaveSettings={saveSettings}
|
||||
currentSettings={currentSettings}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from 'react';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { ChevronDown, Settings, Save, AlertCircle, Check } from 'lucide-react';
|
||||
|
||||
interface Config {
|
||||
export interface Config {
|
||||
prefix: string;
|
||||
split: string;
|
||||
command: string;
|
||||
@ -11,9 +11,10 @@ interface Config {
|
||||
skip_url: boolean;
|
||||
use_tsf_reconvert: boolean;
|
||||
skip_on_out_of_vrc: boolean;
|
||||
tsf_announce?: boolean;
|
||||
}
|
||||
|
||||
enum OnCopyMode {
|
||||
export enum OnCopyMode {
|
||||
ReturnToClipboard = 'ReturnToClipboard',
|
||||
ReturnToChatbox = 'ReturnToChatbox',
|
||||
SendDirectly = 'SendDirectly'
|
||||
@ -57,9 +58,10 @@ interface CheckboxFieldProps {
|
||||
label: React.ReactNode;
|
||||
checked: boolean;
|
||||
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="flex items-start">
|
||||
<div className="flex items-center h-4">
|
||||
@ -69,11 +71,16 @@ const CheckboxField: React.FC<CheckboxFieldProps> = ({ id, name, label, checked,
|
||||
name={name}
|
||||
checked={checked}
|
||||
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 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>
|
||||
</div>
|
||||
@ -81,7 +88,11 @@ const CheckboxField: React.FC<CheckboxFieldProps> = ({ id, name, label, checked,
|
||||
</div>
|
||||
);
|
||||
|
||||
const SettingsComponent: React.FC = () => {
|
||||
interface SettingsComponentProps {
|
||||
setShowTsfModal: (show: boolean) => void;
|
||||
}
|
||||
|
||||
const SettingsComponent: React.FC<SettingsComponentProps> = ({ setShowTsfModal }) => {
|
||||
const [settings, setSettings] = useState<Config>({
|
||||
prefix: ';',
|
||||
split: '/',
|
||||
@ -136,6 +147,30 @@ const SettingsComponent: React.FC = () => {
|
||||
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 newSettings = { ...settings, on_copy_mode: value };
|
||||
setSettings(newSettings);
|
||||
@ -193,6 +228,7 @@ const SettingsComponent: React.FC = () => {
|
||||
label="区切り文字"
|
||||
value={settings.split}
|
||||
onChange={handleChange}
|
||||
disabled={settings.use_tsf_reconvert}
|
||||
description="複数の変換モードを使いたい場合の区切り文字"
|
||||
/>
|
||||
|
||||
@ -201,6 +237,7 @@ const SettingsComponent: React.FC = () => {
|
||||
label="モード変更文字"
|
||||
value={settings.command}
|
||||
onChange={handleChange}
|
||||
disabled={settings.use_tsf_reconvert}
|
||||
description="変換モードを変更するための文字"
|
||||
/>
|
||||
|
||||
@ -210,6 +247,7 @@ const SettingsComponent: React.FC = () => {
|
||||
label="無条件で変換"
|
||||
checked={settings.ignore_prefix}
|
||||
onChange={handleChange}
|
||||
disabled={settings.use_tsf_reconvert}
|
||||
/>
|
||||
|
||||
<InputField
|
||||
@ -217,7 +255,7 @@ const SettingsComponent: React.FC = () => {
|
||||
label="開始文字"
|
||||
value={settings.prefix}
|
||||
onChange={handleChange}
|
||||
disabled={settings.ignore_prefix}
|
||||
disabled={settings.ignore_prefix || settings.use_tsf_reconvert}
|
||||
description="変換を開始する文字(無条件で変換がオンの場合は無効)"
|
||||
/>
|
||||
|
||||
@ -282,7 +320,7 @@ const SettingsComponent: React.FC = () => {
|
||||
</span>
|
||||
}
|
||||
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">
|
||||
Windows10/11では「以前のバージョンの Microsoft IME を使う」を有効化する必要があります。有効にすると区切り、モード変更、開始文字が無効化されます。
|
||||
|
97
src/TsfSettingsModal.tsx
Normal file
97
src/TsfSettingsModal.tsx
Normal 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;
|
Reference in New Issue
Block a user