diff --git a/package.json b/package.json index 303195e..3d0f241 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "vrclipboard-ime-gui", "private": true, - "version": "1.7.0", + "version": "1.8.0", "type": "module", "scripts": { "dev": "vite", diff --git a/release.json b/release.json index de2c321..6933375 100644 --- a/release.json +++ b/release.json @@ -1,7 +1,7 @@ { - "url": "https://r2-vrime.mii.dev/releases/vrclipboard-ime-gui_1.6.0_x64_ja-JP.msi.zip", - "version": "1.6.0", - "notes": "ベータ機能 Text Services Framework を使用した再変換を追加", - "pub_date": "2024-09-23T07:59:37+00:00", - "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTTStkVlpUR0NpcVdnb0phVVBYd2dYWjlHbXdCRGloZzJaZ21aejR2UjgyYXlkWnpvTUI1aERjellPc2lYVHhqaWUrWmdQUXZTMkR4MkVPMWdROHM3RlN2cUFDV3ZBNFE0PQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzI3MDc4MzExCWZpbGU6dnJjbGlwYm9hcmQtaW1lLWd1aV8xLjYuMF94NjRfamEtSlAubXNpLnppcAo2WXhDUWlUNkNtamJmdkloaXRBUEEzdmhvTnJubDQ3Q3dNaXk0OGZISnU4WmM0eVp1NG9KMS9RUUgyY25pd01uWGlCVnpYUDlVc0tIYVlVRU10NzhBdz09Cg==" + "url": "https://r2-vrime.mii.dev/releases/vrclipboard-ime-gui_1.7.0_x64_ja-JP.msi.zip", + "version": "1.7.0", + "notes": "Text Services Framework 再変換のバグ修正および仕様変更", + "pub_date": "2024-09-25T12:10:54+00:00", + "signature": "dW50cnVzdGVkIGNvbW1lbnQ6IHNpZ25hdHVyZSBmcm9tIHRhdXJpIHNlY3JldCBrZXkKUlVTTStkVlpUR0NpcVlQZFdBNEJzYk1QbVl0WUx3YU03SG1KTlJHT05jbVJwSFJ5UFJOaWNxRUd2bnlTOG1uYlo4VGhPc0xieXJGcUk2cDNDdzU4dEVZeVZhRU5QL2tKS3dNPQp0cnVzdGVkIGNvbW1lbnQ6IHRpbWVzdGFtcDoxNzI3MjY2NDI3CWZpbGU6dnJjbGlwYm9hcmQtaW1lLWd1aV8xLjcuMF94NjRfamEtSlAubXNpLnppcApVSyt0VDVyRWl4K1IySTlmNEZWRDEvMVVpRzc2TEdhdyt0WmpvN3FCQm1wN1hpeXZDaUo5WmltM1dsV25raHRkRXNmbW9xazdRb25na29sL1d6MVhCdz09Cg==" } diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 7a52ee2..1582b7f 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1987,6 +1987,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "objc" version = "0.2.7" @@ -3535,7 +3544,9 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa 1.0.11", + "libc", "num-conv", + "num_threads", "powerfmt", "serde", "time-core", @@ -3706,6 +3717,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.27" @@ -3738,6 +3761,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.18" @@ -3748,12 +3781,16 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", + "time", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -3866,7 +3903,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vrclipboard-ime-gui" -version = "1.6.0" +version = "1.8.0" dependencies = [ "anyhow", "calc", @@ -3884,6 +3921,9 @@ dependencies = [ "serde_yaml", "tauri", "tauri-build", + "tracing", + "tracing-appender", + "tracing-subscriber", "windows 0.58.0", "windows-core 0.58.0", ] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3fcf6a7..2d4c9ed 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vrclipboard-ime-gui" -version = "1.6.0" +version = "1.8.0" description = "VRClipboard IME" authors = ["mii"] edition = "2021" @@ -27,6 +27,12 @@ once_cell = "1.19.0" rosc = "~0.10" regex = "1" windows-core = "0.58.0" +tracing = "0.1" +tracing-appender = "0.2" + +[dependencies.tracing-subscriber] +version = "0.3.16" +features = ["env-filter", "fmt", "json", "local-time", "time"] [dependencies.windows] version = "0.58.0" diff --git a/src-tauri/src/com.rs b/src-tauri/src/com.rs index 1936a51..872893e 100644 --- a/src-tauri/src/com.rs +++ b/src-tauri/src/com.rs @@ -1,11 +1,16 @@ use anyhow::Result; +use tracing::debug; use windows::Win32::System::Com::{CoInitialize, CoUninitialize}; pub struct Com; impl Drop for Com { fn drop(&mut self) { - unsafe { CoUninitialize() }; + debug!("Dropping Com instance"); + unsafe { + CoUninitialize(); + debug!("CoUninitialize called"); + }; } } diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index ea3a56f..f7c5633 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -5,6 +5,7 @@ use serde::Serialize; use serde_derive::Deserialize; use anyhow::Result; use tauri::State; +use tracing::{info, error, debug, trace}; use crate::AppState; @@ -64,56 +65,71 @@ impl Default for OnCopyMode { impl Config { pub fn load() -> Result { + debug!("Loading config"); std::fs::create_dir_all(Self::get_path()).unwrap(); - if !Path::new(&Self::get_path().join("config.yaml")).exists() { + let config_path = Self::get_path().join("config.yaml"); + if !Path::new(&config_path).exists() { + info!("Config file not found, generating default"); Self::generate_default_config()?; } - let mut file = File::open(&Self::get_path().join("config.yaml"))?; + let mut file = File::open(&config_path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; + trace!("Raw config contents: {}", contents); let config: Config = serde_yaml::from_str(&contents)?; + debug!("Config loaded successfully"); Ok(config) } pub fn save(&self, state: State) -> Result<(), String> { + debug!("Saving config"); std::fs::create_dir_all(Self::get_path()).unwrap(); - let mut file = match File::create(&Self::get_path().join("config.yaml")) { + let config_path = Self::get_path().join("config.yaml"); + let mut file = match File::create(&config_path) { Ok(file) => file, Err(e) => { - println!("Err: {:?}", e); + error!("Failed to create config file: {}", e); return Err(format!("Failed to create config file: {}", e)) }, }; match serde_yaml::to_string(&self) { Ok(yaml) => { + trace!("Config to be saved: {}", yaml); if let Err(e) = file.write_all(yaml.as_bytes()) { - println!("Err: {:?}", e); + error!("Failed to write config: {}", e); return Err(format!("Failed to write config: {}", e)); } let mut app_config = state.config.lock().unwrap(); *app_config = self.clone(); + info!("Config saved successfully"); Ok(()) }, Err(e) => { - println!("Err: {:?}", e); + error!("Failed to serialize config: {}", e); Err(format!("Failed to serialize config: {}", e)) }, } } pub fn generate_default_config() -> Result<()> { - let mut file = File::create(&Self::get_path().join("config.yaml"))?; - file.write_all(serde_yaml::to_string(&Config::default()).unwrap().as_bytes())?; + debug!("Generating default config"); + let config_path = Self::get_path().join("config.yaml"); + let mut file = File::create(&config_path)?; + let default_config = Config::default(); + let yaml = serde_yaml::to_string(&default_config).unwrap(); + file.write_all(yaml.as_bytes())?; file.flush()?; + info!("Default config generated successfully"); Ok(()) } pub fn get_path() -> PathBuf { let app_dirs = AppDirs::new(Some("vrclipboard-ime"), false).unwrap(); let app_data = app_dirs.config_dir; + trace!("Config path: {:?}", app_data); app_data } } diff --git a/src-tauri/src/conversion.rs b/src-tauri/src/conversion.rs index 8df3128..ae1fe6f 100644 --- a/src-tauri/src/conversion.rs +++ b/src-tauri/src/conversion.rs @@ -1,5 +1,6 @@ use crate::{config::Config, converter::converter::{get_custom_converter, Converter}, STATE}; use anyhow::Result; +use tracing::{info, debug, trace, warn}; pub struct ConversionBlock { pub text: String, @@ -10,69 +11,96 @@ pub struct Conversion; impl Conversion { pub fn new() -> Self { + info!("Creating new Conversion instance"); Self {} } pub fn convert_text(&self, text: &str) -> Result { - println!("Processing text: {}", text); + info!("Converting text: {}", text); + trace!("Text length: {}", text.len()); let blocks = self.split_text(text)?; + trace!("Number of blocks after splitting: {}", blocks.len()); self.convert_blocks(blocks) } pub fn convert_blocks(&self, blocks: Vec) -> Result { + debug!("Converting blocks"); let mut result = String::new(); - for block in blocks { + for (index, block) in blocks.iter().enumerate() { + trace!("Processing block {}/{}", index + 1, blocks.len()); let converted = self.convert_block(&block)?; - println!(" {}: {} -> {}", block.converter.name(), block.text, converted); + debug!("Converted block - {}: {} -> {}", block.converter.name(), block.text, converted); result.push_str(&converted); + trace!("Current result length: {}", result.len()); } - println!(" {}", result); + trace!("Final conversion result: {}", result); Ok(result) } pub fn convert_block(&self, block: &ConversionBlock) -> Result { - if block.text == "" { + trace!("Converting block: {}", block.text); + trace!("Using converter: {}", block.converter.name()); + if block.text.is_empty() { + trace!("Empty block, returning default string"); return Ok(String::default()); } - block.converter.convert(&block.text) + let result = block.converter.convert(&block.text); + trace!("Conversion result: {:?}", result); + result } pub fn split_text(&self, text: &str) -> Result> { + debug!("Splitting text: {}", text); let mut text = text.to_string(); let mut blocks = Vec::new(); let mut current_converter = 'r'; let config = self.get_config(); + trace!("Config command: {}, split: {}", config.command, config.split); if text.starts_with(&config.command) { + trace!("Text starts with command"); text = text.split_off(1); - if text.len() != 0 { + if !text.is_empty() { current_converter = text.chars().next().unwrap_or('n'); text = text.split_off(1); + trace!("Initial converter set to: {}", current_converter); } } for (i, command_splitted) in text.split(&config.command).enumerate() { + trace!("Processing split {}", i); let mut command_splitted = command_splitted.to_string(); if i != 0 { - if command_splitted.len() != 0 { + if !command_splitted.is_empty() { current_converter = command_splitted.chars().next().unwrap_or('n'); command_splitted = command_splitted.split_off(1); + trace!("Converter changed to: {}", current_converter); } } for splitted in command_splitted.split(&config.split) { + trace!("Creating ConversionBlock - text: {}, converter: {}", splitted, current_converter); + let converter = get_custom_converter(current_converter).unwrap_or_else(|| { + warn!("Failed to get custom converter for '{}', using default", current_converter); + get_custom_converter('n').unwrap() + }); blocks.push(ConversionBlock { text: splitted.to_string(), - converter: get_custom_converter(current_converter).unwrap_or(get_custom_converter('n').unwrap()) + converter }); } } + debug!("Split text into {} blocks", blocks.len()); + trace!("Blocks: {:?}", blocks.iter().map(|b| &b.text).collect::>()); Ok(blocks) } pub fn get_config(&self) -> Config { - STATE.lock().unwrap().clone() + trace!("Getting config"); + let config = STATE.lock().unwrap().clone(); + trace!("Config retrieved: {:?}", config); + config } } diff --git a/src-tauri/src/converter/calculator.rs b/src-tauri/src/converter/calculator.rs index 0fe2131..7a0b363 100644 --- a/src-tauri/src/converter/calculator.rs +++ b/src-tauri/src/converter/calculator.rs @@ -1,4 +1,5 @@ use calc::Context; +use tracing::{debug, info, trace}; use super::converter::Converter; @@ -6,16 +7,25 @@ pub struct CalculatorConverter; impl Converter for CalculatorConverter { fn convert(&self, text: &str) -> anyhow::Result { + debug!("Evaluating expression: {}", text); let mut ctx = Context::::default(); let result = match ctx.evaluate(text) { - Ok(result) => format!("{} = {}", text, result.to_string()), - Err(e) => e.to_string(), + Ok(result) => { + let formatted = format!("{} = {}", text, result.to_string()); + info!("Evaluation successful: {}", formatted); + formatted + }, + Err(e) => { + debug!("Evaluation failed: {}", e); + e.to_string() + }, }; Ok(result) } fn name(&self) -> String { + trace!("Getting converter name"); "calculator".to_string() } } diff --git a/src-tauri/src/converter/converter.rs b/src-tauri/src/converter/converter.rs index e6537e8..e4df504 100644 --- a/src-tauri/src/converter/converter.rs +++ b/src-tauri/src/converter/converter.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use tracing::{debug, info, trace}; use super::{calculator::CalculatorConverter, hiragana::HiraganaConverter, katakana::KatakanaConverter, none_converter::NoneConverter, roman_to_kanji::RomanToKanjiConverter}; @@ -8,12 +9,18 @@ pub trait Converter { } pub fn get_custom_converter(prefix: char) -> Option> { - match prefix { + debug!("Getting custom converter for prefix: {}", prefix); + let converter = match prefix { 'r' => Some(Box::new(RomanToKanjiConverter) as Box), 'h' => Some(Box::new(HiraganaConverter) as Box), 'c' => Some(Box::new(CalculatorConverter) as Box), 'n' => Some(Box::new(NoneConverter) as Box), 'k' => Some(Box::new(KatakanaConverter) as Box), _ => None, + }; + match &converter { + Some(c) => debug!("Custom converter found: {}", c.name()), + None => trace!("No custom converter found for prefix: {}", prefix), } + converter } diff --git a/src-tauri/src/converter/hiragana.rs b/src-tauri/src/converter/hiragana.rs index e895559..688e0ef 100644 --- a/src-tauri/src/converter/hiragana.rs +++ b/src-tauri/src/converter/hiragana.rs @@ -1,4 +1,5 @@ use windows::Win32::UI::Input::Ime::{FELANG_CMODE_HIRAGANAOUT, FELANG_CMODE_NOINVISIBLECHAR, FELANG_CMODE_PRECONV, FELANG_REQ_REV}; +use tracing::{debug, info, trace}; use crate::felanguage::FElanguage; @@ -9,11 +10,22 @@ pub struct HiraganaConverter; impl Converter for HiraganaConverter { fn convert(&self, text: &str) -> anyhow::Result { + debug!("Converting to hiragana: {}", text); let felanguage = FElanguage::new()?; - felanguage.j_morph_result(text, FELANG_REQ_REV, FELANG_CMODE_HIRAGANAOUT | FELANG_CMODE_PRECONV | FELANG_CMODE_NOINVISIBLECHAR) + trace!("FElanguage instance created"); + + let result = felanguage.j_morph_result(text, FELANG_REQ_REV, FELANG_CMODE_HIRAGANAOUT | FELANG_CMODE_PRECONV | FELANG_CMODE_NOINVISIBLECHAR); + + match &result { + Ok(converted) => info!("Conversion successful: {} -> {}", text, converted), + Err(e) => debug!("Conversion failed: {}", e), + } + + result } fn name(&self) -> String { + trace!("Getting converter name"); "hiragana".to_string() } } diff --git a/src-tauri/src/converter/katakana.rs b/src-tauri/src/converter/katakana.rs index 48886a7..619c7d2 100644 --- a/src-tauri/src/converter/katakana.rs +++ b/src-tauri/src/converter/katakana.rs @@ -1,4 +1,5 @@ use windows::Win32::UI::Input::Ime::{FELANG_CMODE_KATAKANAOUT, FELANG_CMODE_NOINVISIBLECHAR, FELANG_CMODE_PRECONV, FELANG_REQ_REV}; +use tracing::{debug, info, trace}; use crate::felanguage::FElanguage; @@ -9,11 +10,22 @@ pub struct KatakanaConverter; impl Converter for KatakanaConverter { fn convert(&self, text: &str) -> anyhow::Result { + debug!("Converting to katakana: {}", text); let felanguage = FElanguage::new()?; - felanguage.j_morph_result(text, FELANG_REQ_REV, FELANG_CMODE_KATAKANAOUT | FELANG_CMODE_PRECONV | FELANG_CMODE_NOINVISIBLECHAR) + trace!("FElanguage instance created"); + + let result = felanguage.j_morph_result(text, FELANG_REQ_REV, FELANG_CMODE_KATAKANAOUT | FELANG_CMODE_PRECONV | FELANG_CMODE_NOINVISIBLECHAR); + + match &result { + Ok(converted) => info!("Conversion successful: {} -> {}", text, converted), + Err(e) => debug!("Conversion failed: {}", e), + } + + result } fn name(&self) -> String { + trace!("Getting converter name"); "katakana".to_string() } } diff --git a/src-tauri/src/converter/none_converter.rs b/src-tauri/src/converter/none_converter.rs index 1054e08..3ab2692 100644 --- a/src-tauri/src/converter/none_converter.rs +++ b/src-tauri/src/converter/none_converter.rs @@ -1,13 +1,16 @@ +use tracing::{debug, trace}; use super::converter::Converter; pub struct NoneConverter; impl Converter for NoneConverter { fn convert(&self, text: &str) -> anyhow::Result { + debug!("Converting with NoneConverter: {}", text); Ok(text.to_string()) } fn name(&self) -> String { + trace!("Getting converter name"); "none".to_string() } } diff --git a/src-tauri/src/converter/roman_to_kanji.rs b/src-tauri/src/converter/roman_to_kanji.rs index e619e29..5026b8d 100644 --- a/src-tauri/src/converter/roman_to_kanji.rs +++ b/src-tauri/src/converter/roman_to_kanji.rs @@ -1,4 +1,5 @@ use windows::Win32::UI::Input::Ime::{FELANG_CMODE_HIRAGANAOUT, FELANG_CMODE_NOINVISIBLECHAR, FELANG_CMODE_PRECONV, FELANG_CMODE_ROMAN, FELANG_REQ_CONV}; +use tracing::{debug, info, trace}; use crate::felanguage::FElanguage; @@ -8,14 +9,25 @@ pub struct RomanToKanjiConverter; impl Converter for RomanToKanjiConverter { fn convert(&self, text: &str) -> anyhow::Result { + debug!("Converting roman to kanji: {}", text); let felanguage = FElanguage::new()?; - felanguage.j_morph_result(text, FELANG_REQ_CONV, FELANG_CMODE_HIRAGANAOUT + trace!("FElanguage instance created"); + + let result = felanguage.j_morph_result(text, FELANG_REQ_CONV, FELANG_CMODE_HIRAGANAOUT | FELANG_CMODE_ROMAN | FELANG_CMODE_NOINVISIBLECHAR - | FELANG_CMODE_PRECONV) + | FELANG_CMODE_PRECONV); + + match &result { + Ok(converted) => info!("Conversion successful: {} -> {}", text, converted), + Err(e) => debug!("Conversion failed: {}", e), + } + + result } fn name(&self) -> String { + trace!("Getting converter name"); "roman_to_kanji".to_string() } } diff --git a/src-tauri/src/felanguage.rs b/src-tauri/src/felanguage.rs index 71e370a..a178bc5 100644 --- a/src-tauri/src/felanguage.rs +++ b/src-tauri/src/felanguage.rs @@ -1,6 +1,7 @@ use std::ptr; use anyhow::Result; +use tracing::{debug, error, info, trace}; use windows::{ core::{w, PCWSTR}, Win32::{ @@ -15,25 +16,41 @@ pub struct FElanguage { impl Drop for FElanguage { fn drop(&mut self) { - unsafe { self.ife.Close().ok() }; + debug!("Dropping FElanguage instance"); + if let Err(e) = unsafe { self.ife.Close() } { + error!("Error closing IFELanguage: {:?}", e); + } } } impl FElanguage { pub fn new() -> Result { - let clsid = unsafe { CLSIDFromProgID(w!("MSIME.Japan"))? }; - let ife: IFELanguage = unsafe { CoCreateInstance(&clsid, None, CLSCTX_ALL)? }; - unsafe { ife.Open()? }; + info!("Creating new FElanguage instance"); + let clsid = unsafe { + trace!("Getting CLSID for MSIME.Japan"); + CLSIDFromProgID(w!("MSIME.Japan"))? + }; + let ife: IFELanguage = unsafe { + trace!("Creating IFELanguage instance"); + CoCreateInstance(&clsid, None, CLSCTX_ALL)? + }; + unsafe { + trace!("Opening IFELanguage"); + ife.Open()? + }; + debug!("FElanguage instance created successfully"); Ok(FElanguage { ife }) } pub fn j_morph_result(&self, input: &str, request: u32, mode: u32) -> Result { + debug!("Calling j_morph_result with input: {}, request: {}, mode: {}", input, request, mode); let input_utf16: Vec = input.encode_utf16().chain(Some(0)).collect(); let input_len = input_utf16.len(); let input_pcwstr = PCWSTR::from_raw(input_utf16.as_ptr()); let mut result_ptr = ptr::null_mut(); unsafe { + trace!("Calling GetJMorphResult"); self.ife.GetJMorphResult( request, mode, @@ -45,6 +62,7 @@ impl FElanguage { } if result_ptr.is_null() { + error!("GetJMorphResult returned null pointer"); return Err(anyhow::anyhow!("GetJMorphResult returned null pointer")); } @@ -53,12 +71,14 @@ impl FElanguage { let output_len = result_struct.cchOutput as usize; if output_bstr_ptr.is_null() { + error!("Output BSTR pointer is null"); return Err(anyhow::anyhow!("Output BSTR pointer is null")); } let output_slice = unsafe { std::slice::from_raw_parts(output_bstr_ptr.as_ptr(), output_len) }; let output_string = String::from_utf16_lossy(output_slice); + trace!("j_morph_result output: {}", output_string); Ok(output_string) } } diff --git a/src-tauri/src/handler.rs b/src-tauri/src/handler.rs index 21eb9a6..38302e6 100644 --- a/src-tauri/src/handler.rs +++ b/src-tauri/src/handler.rs @@ -8,6 +8,7 @@ use rosc::{encoder, OscMessage, OscPacket, OscType}; use tauri::{AppHandle, Manager}; use crate::{config::{Config, OnCopyMode}, conversion::Conversion, tsf_conversion::TsfConversion, Log, STATE}; use anyhow::Result; +use tracing::{info, warn, error}; pub struct ConversionHandler { app_handle: AppHandle, @@ -23,6 +24,7 @@ impl ConversionHandler { let tsf_conversion = None; let clipboard_ctx = ClipboardProvider::new().unwrap(); + info!("ConversionHandler created"); Ok(Self { app_handle, conversion, tsf_conversion, clipboard_ctx, last_text: String::new() }) } @@ -34,23 +36,24 @@ impl ConversionHandler { impl ConversionHandler { fn tsf_conversion(&mut self, contents: &str, config: &Config) -> Result<()> { if contents.chars().count() > 140 { + info!("Content exceeds 140 characters, skipping TSF conversion"); return Ok(()); } if config.skip_url && Regex::new(r"(http://|https://){1}[\w\.\-/:\#\?=\&;%\~\+]+").unwrap().is_match(&contents) { + info!("URL detected, skipping TSF conversion"); return Ok(()); } if self.tsf_conversion.is_none() { self.tsf_conversion = Some(TsfConversion::new()); - - println!("TSF conversion created."); + info!("TSF conversion created"); } let tsf_conversion = self.tsf_conversion.as_mut().unwrap(); let converted = tsf_conversion.convert(contents)?; - println!("TSF conversion: {} -> {}", contents, converted); + info!("TSF conversion: {} -> {}", contents, converted); self.last_text = contents.to_string().clone(); @@ -65,10 +68,12 @@ impl ConversionHandler { let mut count = 0; while self.clipboard_ctx.set_contents(converted.clone()).is_err() { if count > 4 { + warn!("Failed to set clipboard contents after 5 attempts"); break; } count += 1; } + info!("Conversion returned to clipboard"); }, OnCopyMode::ReturnToChatbox => { let sock = UdpSocket::bind("127.0.0.1:0").unwrap(); @@ -81,7 +86,11 @@ impl ConversionHandler { ] })).unwrap(); - sock.send_to(&msg_buf, "127.0.0.1:9000").unwrap(); + if let Err(e) = sock.send_to(&msg_buf, "127.0.0.1:9000") { + error!("Failed to send UDP packet: {}", e); + } else { + info!("Conversion returned to chatbox"); + } }, OnCopyMode::SendDirectly => { let sock = UdpSocket::bind("127.0.0.1:0").unwrap(); @@ -94,7 +103,11 @@ impl ConversionHandler { ] })).unwrap(); - sock.send_to(&msg_buf, "127.0.0.1:9000").unwrap(); + if let Err(e) = sock.send_to(&msg_buf, "127.0.0.1:9000") { + error!("Failed to send UDP packet: {}", e); + } else { + info!("Conversion sent directly"); + } }, } @@ -105,7 +118,7 @@ impl ConversionHandler { original: parsed_contents, converted }).is_err() { - println!("App handle add log failed."); + error!("App handle add log failed"); } } } @@ -115,14 +128,16 @@ impl ClipboardHandler for ConversionHandler { let config = self.get_config(); if let Ok(mut contents) = self.clipboard_ctx.get_contents() { if config.use_tsf_reconvert { - self.tsf_conversion(&contents, &config).expect("TSF conversion failed."); + if let Err(e) = self.tsf_conversion(&contents, &config) { + error!("TSF conversion failed: {}", e); + } return CallbackResult::Next; } if contents != self.last_text { if contents.starts_with(&config.prefix) || config.ignore_prefix { - if config.skip_url && Regex::new(r"(http://|https://){1}[\w\.\-/:\#\?=\&;%\~\+]+").unwrap().is_match(&contents) { + info!("URL detected, skipping conversion"); return CallbackResult::Next; } @@ -130,7 +145,7 @@ impl ClipboardHandler for ConversionHandler { let converted = match self.conversion.convert_text(&parsed_contents) { Ok(converted) => converted, Err(err) => { - println!("Error: {:?}", err); + error!("Conversion error: {:?}", err); format!("Error: {:?}", err) } }; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 3c99c2f..ca6b262 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -21,6 +21,7 @@ use clipboard_master::Master; use com::Com; use config::Config; use handler::ConversionHandler; +use tracing::Level; #[derive(Serialize, Deserialize, Debug, Clone)] struct Log { @@ -55,6 +56,7 @@ fn save_settings(config: Config, state: State) -> Result<(), String> { fn main() { println!("VRClipboard-IME Logs\nバグがあった場合はこのログを送ってください。"); + tracing_subscriber::fmt().with_max_level(Level::TRACE).init(); tauri::Builder::default() .manage(AppState { config: Mutex::new(Config::load().unwrap_or_else(|_| { @@ -64,6 +66,7 @@ fn main() { }) .invoke_handler(tauri::generate_handler![load_settings, save_settings]) .setup(|app| { + let _span = tracing::span!(tracing::Level::INFO, "main"); app.manage(STATE.lock().unwrap().clone()); let app_handle = app.app_handle(); diff --git a/src-tauri/src/tsf/function_provider.rs b/src-tauri/src/tsf/function_provider.rs index c2724a5..59918a4 100644 --- a/src-tauri/src/tsf/function_provider.rs +++ b/src-tauri/src/tsf/function_provider.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use tracing::{debug, info, error}; use windows::{ core::Interface, Win32::UI::TextServices::{ITfFnSearchCandidateProvider, ITfFunctionProvider}, @@ -12,12 +13,31 @@ pub struct FunctionProvider { impl FunctionProvider { pub fn new(function_provider: ITfFunctionProvider) -> Self { + debug!("Creating new FunctionProvider"); Self { function_provider } } pub fn get_search_candidate_provider(&self) -> Result { + debug!("Getting search candidate provider"); let zeroed_guid = windows_core::GUID::zeroed(); - let search_candidate_provider = unsafe { self.function_provider.GetFunction(&zeroed_guid, &ITfFnSearchCandidateProvider::IID)? }; - Ok(SearchCandidateProvider::new(search_candidate_provider.cast()?)) + match unsafe { self.function_provider.GetFunction(&zeroed_guid, &ITfFnSearchCandidateProvider::IID) } { + Ok(search_candidate_provider) => { + info!("Search candidate provider obtained successfully"); + match search_candidate_provider.cast() { + Ok(provider) => { + debug!("Successfully cast search candidate provider"); + Ok(SearchCandidateProvider::new(provider)) + }, + Err(e) => { + error!("Failed to cast search candidate provider: {:?}", e); + Err(e.into()) + } + } + }, + Err(e) => { + error!("Failed to get search candidate provider: {:?}", e); + Err(e.into()) + } + } } } diff --git a/src-tauri/src/tsf/input_processor_profile_mgr.rs b/src-tauri/src/tsf/input_processor_profile_mgr.rs index 5ac79ca..787e36d 100644 --- a/src-tauri/src/tsf/input_processor_profile_mgr.rs +++ b/src-tauri/src/tsf/input_processor_profile_mgr.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use tracing::{debug, info, error}; use windows::Win32::{ System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER}, UI::{Input::KeyboardAndMouse::HKL, TextServices::{CLSID_TF_InputProcessorProfiles, ITfInputProcessorProfileMgr, GUID_TFCAT_TIP_KEYBOARD, TF_INPUTPROCESSORPROFILE, TF_IPPMF_DONTCARECURRENTINPUTLANGUAGE, TF_PROFILETYPE_INPUTPROCESSOR}}, @@ -10,21 +11,40 @@ pub struct InputProcessorProfileMgr { impl InputProcessorProfileMgr { pub fn new() -> Result { + debug!("Creating new InputProcessorProfileMgr"); let input_processor_profile_mgr = unsafe { CoCreateInstance(&CLSID_TF_InputProcessorProfiles, None, CLSCTX_INPROC_SERVER)? }; + info!("InputProcessorProfileMgr created successfully"); Ok(InputProcessorProfileMgr { input_processor_profile_mgr }) } pub fn get_active_profile(&self) -> Result { + debug!("Getting active profile"); let keyboard_guid = GUID_TFCAT_TIP_KEYBOARD; let mut profile = TF_INPUTPROCESSORPROFILE::default(); - unsafe { self.input_processor_profile_mgr.GetActiveProfile(&keyboard_guid, &mut profile)? }; - - Ok(profile) + match unsafe { self.input_processor_profile_mgr.GetActiveProfile(&keyboard_guid, &mut profile) } { + Ok(_) => { + info!("Active profile retrieved successfully"); + Ok(profile) + }, + Err(e) => { + error!("Failed to get active profile: {:?}", e); + Err(e.into()) + } + } } pub fn activate_profile(&self, profile: &TF_INPUTPROCESSORPROFILE) -> Result<()> { - unsafe { self.input_processor_profile_mgr.ActivateProfile(TF_PROFILETYPE_INPUTPROCESSOR, profile.langid, &profile.clsid, &profile.guidProfile, HKL::default(), TF_IPPMF_DONTCARECURRENTINPUTLANGUAGE)? }; - Ok(()) + debug!("Activating profile: {:?}", profile); + match unsafe { self.input_processor_profile_mgr.ActivateProfile(TF_PROFILETYPE_INPUTPROCESSOR, profile.langid, &profile.clsid, &profile.guidProfile, HKL::default(), TF_IPPMF_DONTCARECURRENTINPUTLANGUAGE) } { + Ok(_) => { + info!("Profile activated successfully"); + Ok(()) + }, + Err(e) => { + error!("Failed to activate profile: {:?}", e); + Err(e.into()) + } + } } } diff --git a/src-tauri/src/tsf/mod.rs b/src-tauri/src/tsf/mod.rs index 01a3a4b..14d4a6c 100644 --- a/src-tauri/src/tsf/mod.rs +++ b/src-tauri/src/tsf/mod.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use tracing::{debug, error}; use windows::Win32::UI::WindowsAndMessaging::{SystemParametersInfoW, SPI_SETTHREADLOCALINPUTSETTINGS, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS}; pub mod input_processor_profile_mgr; @@ -7,8 +8,16 @@ pub mod search_candidate_provider; pub mod thread_mgr; pub fn set_thread_local_input_settings(thread_local_input_settings: bool) -> Result<()> { + debug!("Setting thread local input settings to: {}", thread_local_input_settings); let mut result = thread_local_input_settings; - unsafe { SystemParametersInfoW(SPI_SETTHREADLOCALINPUTSETTINGS, 0, Some(&mut result as *mut _ as *const _ as *mut _), SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0))? }; - - Ok(()) + match unsafe { SystemParametersInfoW(SPI_SETTHREADLOCALINPUTSETTINGS, 0, Some(&mut result as *mut _ as *const _ as *mut _), SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0)) } { + Ok(_) => { + debug!("Successfully set thread local input settings"); + Ok(()) + }, + Err(e) => { + error!("Failed to set thread local input settings: {:?}", e); + Err(e.into()) + } + } } diff --git a/src-tauri/src/tsf/search_candidate_provider.rs b/src-tauri/src/tsf/search_candidate_provider.rs index c525db3..3def0cf 100644 --- a/src-tauri/src/tsf/search_candidate_provider.rs +++ b/src-tauri/src/tsf/search_candidate_provider.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use tracing::{debug, info, error, trace}; use windows::Win32::UI::TextServices::{ITfFnSearchCandidateProvider, TF_TMAE_NOACTIVATEKEYBOARDLAYOUT}; use super::{function_provider::FunctionProvider, input_processor_profile_mgr::InputProcessorProfileMgr, thread_mgr::ThreadMgr}; @@ -9,41 +10,60 @@ pub struct SearchCandidateProvider { impl SearchCandidateProvider { pub fn new(search_candidate_provider: ITfFnSearchCandidateProvider) -> Self { + debug!("Creating new SearchCandidateProvider"); Self { search_candidate_provider } } pub fn create() -> Result { + info!("Creating SearchCandidateProvider"); let profile_mgr = InputProcessorProfileMgr::new()?; let profile = profile_mgr.get_active_profile()?; + debug!("Activating profile"); profile_mgr.activate_profile(&profile)?; + debug!("Creating ThreadMgr"); let thread_mgr = ThreadMgr::new()?; let _client_id = thread_mgr.activate_ex(TF_TMAE_NOACTIVATEKEYBOARDLAYOUT)?; + debug!("Getting function provider"); let function_provider = thread_mgr.get_function_provider(&profile.clsid)?; + debug!("Getting search candidate provider"); let search_candidate_provider = FunctionProvider::new(function_provider).get_search_candidate_provider()?; + info!("SearchCandidateProvider created successfully"); Ok(search_candidate_provider) } pub fn get_candidates(&self, input: &str, max: usize) -> Result> { + debug!("Getting candidates for input: {}, max: {}", input, max); let input_utf16: Vec = input.encode_utf16().chain(Some(0)).collect(); let input_bstr = windows_core::BSTR::from_wide(&input_utf16)?; let input_utf16: Vec = "".encode_utf16().chain(Some(0)).collect(); let input_bstr_empty = windows_core::BSTR::from_wide(&input_utf16)?; + trace!("Calling GetSearchCandidates"); let candidates = unsafe { self.search_candidate_provider.GetSearchCandidates(&input_bstr, &input_bstr_empty)? }; let candidates_enum = unsafe { candidates.EnumCandidates()? }; let mut candidates = vec![None; max]; let mut candidates_count = 0; + trace!("Enumerating candidates"); unsafe { candidates_enum.Next(&mut candidates, &mut candidates_count)? }; candidates.resize(candidates_count as usize, None); - let candidates: Vec = candidates.iter().map(|candidate| unsafe { candidate.as_ref().unwrap().GetString().unwrap().to_string() }).collect(); + let candidates: Vec = candidates.iter().map(|candidate| unsafe { + match candidate.as_ref().unwrap().GetString() { + Ok(s) => s.to_string(), + Err(e) => { + error!("Failed to get candidate string: {:?}", e); + String::new() + } + } + }).collect(); + info!("Retrieved {} candidates", candidates.len()); Ok(candidates) } } diff --git a/src-tauri/src/tsf/thread_mgr.rs b/src-tauri/src/tsf/thread_mgr.rs index 6f67d99..24c695c 100644 --- a/src-tauri/src/tsf/thread_mgr.rs +++ b/src-tauri/src/tsf/thread_mgr.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use tracing::{debug, error, info}; use windows::Win32::{ System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER}, UI::TextServices::{CLSID_TF_ThreadMgr, ITfFunctionProvider, ITfThreadMgr2}, @@ -10,17 +11,31 @@ pub struct ThreadMgr { impl ThreadMgr { pub fn new() -> Result { + debug!("Creating new ThreadMgr"); let thread_mgr = unsafe { CoCreateInstance(&CLSID_TF_ThreadMgr, None, CLSCTX_INPROC_SERVER)? }; + info!("ThreadMgr created successfully"); Ok(ThreadMgr { thread_mgr }) } pub fn activate_ex(&self, flags: u32) -> Result { + debug!("Activating ThreadMgr with flags: {}", flags); let mut client_id = 0; unsafe { self.thread_mgr.ActivateEx(&mut client_id as *mut _ as *const _ as *mut _, flags)? }; + info!("ThreadMgr activated with client_id: {}", client_id); Ok(client_id) } pub fn get_function_provider(&self, clsid: &windows_core::GUID) -> Result { - Ok(unsafe { self.thread_mgr.GetFunctionProvider(clsid)? }) + debug!("Getting function provider for CLSID: {:?}", clsid); + match unsafe { self.thread_mgr.GetFunctionProvider(clsid) } { + Ok(provider) => { + info!("Function provider obtained successfully"); + Ok(provider) + } + Err(e) => { + error!("Failed to get function provider: {:?}", e); + Err(e.into()) + } + } } } diff --git a/src-tauri/src/tsf_conversion.rs b/src-tauri/src/tsf_conversion.rs index 83fcc39..7f6da89 100644 --- a/src-tauri/src/tsf_conversion.rs +++ b/src-tauri/src/tsf_conversion.rs @@ -1,4 +1,5 @@ use anyhow::Result; +use tracing::{info, debug, error, trace}; use crate::{converter::{converter::Converter, hiragana::HiraganaConverter, roman_to_kanji::RomanToKanjiConverter}, tsf::{search_candidate_provider::SearchCandidateProvider, set_thread_local_input_settings}}; pub struct TsfConversion { @@ -14,9 +15,10 @@ pub struct TsfConversion { impl TsfConversion { pub fn new() -> Self { + info!("Creating new TsfConversion instance"); set_thread_local_input_settings(true).unwrap(); - Self { + let instance = Self { conversion_history: Vec::new(), clipboard_history: Vec::new(), now_reconvertion: false, @@ -25,55 +27,73 @@ impl TsfConversion { reconversion_candidates: None, reconversion_index: None, reconversion_prefix: None, - } + }; + instance } fn reset_conversion_state(&mut self) { + debug!("Resetting conversion state"); + trace!("Before reset - now_reconvertion: {}, reconversion_prefix: {:?}, reconversion_index: {:?}, reconversion_candidates: {:?}", + self.now_reconvertion, self.reconversion_prefix, self.reconversion_index, self.reconversion_candidates); self.now_reconvertion = false; self.reconversion_prefix = None; self.reconversion_index = None; self.reconversion_candidates = None; + trace!("After reset - now_reconvertion: {}, reconversion_prefix: {:?}, reconversion_index: {:?}, reconversion_candidates: {:?}", + self.now_reconvertion, self.reconversion_prefix, self.reconversion_index, self.reconversion_candidates); } fn convert_roman_to_kanji(&mut self, text: &str) -> Result { + debug!("Converting roman to kanji: {}", text); let o_minus_1 = self.conversion_history.get(if self.conversion_history.len() > 0 { self.conversion_history.len() - 1 } else { 0 }).unwrap_or(&("".to_string())).clone(); + trace!("Previous conversion (o_minus_1): {}", o_minus_1); let mut first_diff_position = o_minus_1.chars().zip(text.chars()).position(|(a, b)| a != b); if o_minus_1 != text && first_diff_position.is_none() { first_diff_position = Some(o_minus_1.chars().count()); } + trace!("First difference position: {:?}", first_diff_position); let diff = text.chars().skip(first_diff_position.unwrap_or(0)).collect::(); + debug!("Difference to convert: {}", diff); let roman_to_kanji_converter = RomanToKanjiConverter; let converted = roman_to_kanji_converter.convert(&diff)?; + trace!("Converted difference: {}", converted); self.conversion_history.push(o_minus_1.chars().zip(text.chars()).take_while(|(a, b)| a == b).map(|(a, _)| a).collect::() + &converted); self.clipboard_history.push(text.to_string()); - return Ok(self.conversion_history.last().unwrap().clone()); + info!("Roman to kanji conversion result: {}", self.conversion_history.last().unwrap()); + trace!("Updated conversion history: {:?}", self.conversion_history); + trace!("Updated clipboard history: {:?}", self.clipboard_history); + Ok(self.conversion_history.last().unwrap().clone()) } fn convert_tsf(&mut self, text: &str) -> Result { + debug!("Converting using TSF: {}", text); self.now_reconvertion = true; let mut diff_hiragana = String::new(); let mut diff = String::new(); if self.reconversion_prefix.is_none() { let o_minus_2 = self.conversion_history.get(if self.conversion_history.len() > 1 { self.conversion_history.len() - 2 } else { 0 }).unwrap_or(&("".to_string())).clone(); let i_minus_1 = self.clipboard_history.get(if self.clipboard_history.len() > 0 { self.clipboard_history.len() - 1 } else { 0 }).unwrap_or(&("".to_string())).clone(); - println!("o,i: {}, {}", o_minus_2, i_minus_1); + trace!("o_minus_2: {}, i_minus_1: {}", o_minus_2, i_minus_1); let mut first_diff_position = i_minus_1.chars().zip(o_minus_2.chars()).position(|(a, b)| a != b); - println!("diff_pos: {:?}", first_diff_position); + trace!("First difference position: {:?}", first_diff_position); if o_minus_2 != i_minus_1 && first_diff_position.is_none() { first_diff_position = Some(o_minus_2.chars().count()); } diff = i_minus_1.chars().skip(first_diff_position.unwrap_or(0)).collect::(); - println!("diff: {}", diff); + debug!("Difference to convert: {}", diff); diff_hiragana = HiraganaConverter.convert(&diff)?; + trace!("Hiragana conversion: {}", diff_hiragana); let prefix = i_minus_1.chars().zip(o_minus_2.chars()).take_while(|(a, b)| a == b).map(|(a, _)| a).collect::(); self.reconversion_prefix = Some(prefix.clone()); + trace!("Set reconversion prefix: {:?}", self.reconversion_prefix); } - println!("diff_hiragana: {}", diff_hiragana); let candidates = self.reconversion_candidates.get_or_insert_with(|| { + debug!("Generating new candidates"); let mut candidates = self.search_candidate_provider.get_candidates(&diff_hiragana, 10).unwrap_or_default(); + trace!("Initial candidates: {:?}", candidates); if candidates.is_empty() { candidates.push(diff_hiragana.clone()); let roman_to_kanji_converter = RomanToKanjiConverter; @@ -81,23 +101,24 @@ impl TsfConversion { candidates.push(roman_to_kanji); } candidates.insert(0, diff.to_string()); + trace!("Final candidates: {:?}", candidates); candidates }); let index = self.reconversion_index.get_or_insert(-1); + trace!("Current reconversion index: {}", index); if *index + 1 < candidates.len() as i32 { *index += 1; } else { *index = 0; } - - if self.reconversion_candidates.is_some() { - println!("Candidates: {:?}", self.reconversion_candidates.as_ref().unwrap()); - } + debug!("Updated reconversion index: {}", index); self.conversion_history.push(self.reconversion_prefix.clone().unwrap() + &self.reconversion_candidates.as_ref().unwrap()[self.reconversion_index.unwrap() as usize].clone()); self.clipboard_history.push(text.to_string()); + trace!("Updated conversion history: {:?}", self.conversion_history); + trace!("Updated clipboard history: {:?}", self.clipboard_history); while self.conversion_history.len() > 3 { self.conversion_history.remove(0); @@ -105,32 +126,39 @@ impl TsfConversion { while self.clipboard_history.len() > 3 { self.clipboard_history.remove(0); } + trace!("Trimmed conversion history: {:?}", self.conversion_history); + trace!("Trimmed clipboard history: {:?}", self.clipboard_history); - return Ok(self.conversion_history.last().unwrap().clone()); + info!("TSF conversion result: {}", self.conversion_history.last().unwrap()); + Ok(self.conversion_history.last().unwrap().clone()) } pub fn convert(&mut self, text: &str) -> Result { - println!(); - println!("History: {:?}, {:?}", self.conversion_history, self.clipboard_history); - println!("{} == {}", text, self.conversion_history.last().unwrap_or(&("".to_string())).clone()); + debug!("Starting conversion for: {}", text); + trace!("Current conversion history: {:?}", self.conversion_history); + trace!("Current clipboard history: {:?}", self.clipboard_history); let same_as_last_conversion = text.to_string() == self.conversion_history.last().unwrap_or(&("".to_string())).clone(); + trace!("Same as last conversion: {}", same_as_last_conversion); self.target_text = text.to_string(); + trace!("Set target text: {}", self.target_text); if !same_as_last_conversion && self.now_reconvertion { + debug!("Resetting conversion state due to new input"); self.reset_conversion_state(); } if !self.now_reconvertion && !same_as_last_conversion { - println!("Convert using roman_to_kanji"); + info!("Converting using roman_to_kanji"); return self.convert_roman_to_kanji(text); } if same_as_last_conversion || self.now_reconvertion { - println!("Convert using TSF"); + info!("Converting using TSF"); return self.convert_tsf(text); } + error!("Failed to convert: {}", text); Err(anyhow::anyhow!("Failed to convert")) } } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6079303..a11b69f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -7,7 +7,7 @@ }, "package": { "productName": "vrclipboard-ime-gui", - "version": "1.7.0" + "version": "1.8.0" }, "tauri": { "updater": { diff --git a/src/SettingsComponent.tsx b/src/SettingsComponent.tsx index 1abebcb..e027dcf 100644 --- a/src/SettingsComponent.tsx +++ b/src/SettingsComponent.tsx @@ -192,7 +192,7 @@ const SettingsComponent = () => { />