update UI

This commit is contained in:
mii
2025-03-04 17:22:02 +09:00
parent ab78a00967
commit dbf984652c
15 changed files with 896 additions and 159 deletions

87
src-tauri/Cargo.lock generated
View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "addr2line"
@ -3577,7 +3577,7 @@ dependencies = [
"tao-macros",
"unicode-segmentation",
"url",
"windows",
"windows 0.58.0",
"windows-core 0.58.0",
"windows-version",
"x11-dl",
@ -3658,7 +3658,7 @@ dependencies = [
"webkit2gtk",
"webview2-com",
"window-vibrancy",
"windows",
"windows 0.58.0",
]
[[package]]
@ -3808,7 +3808,7 @@ dependencies = [
"tauri-utils",
"thiserror",
"url",
"windows",
"windows 0.58.0",
]
[[package]]
@ -3833,7 +3833,7 @@ dependencies = [
"url",
"webkit2gtk",
"webview2-com",
"windows",
"windows 0.58.0",
"wry",
]
@ -4422,8 +4422,8 @@ dependencies = [
"tracing",
"tracing-appender",
"tracing-subscriber",
"windows",
"windows-core 0.58.0",
"windows 0.56.0",
"windows-core 0.56.0",
]
[[package]]
@ -4627,10 +4627,10 @@ checksum = "6f61ff3d9d0ee4efcb461b14eb3acfda2702d10dc329f339303fc3e57215ae2c"
dependencies = [
"webview2-com-macros",
"webview2-com-sys",
"windows",
"windows 0.58.0",
"windows-core 0.58.0",
"windows-implement",
"windows-interface",
"windows-implement 0.58.0",
"windows-interface 0.58.0",
]
[[package]]
@ -4651,7 +4651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886"
dependencies = [
"thiserror",
"windows",
"windows 0.58.0",
"windows-core 0.58.0",
]
@ -4700,6 +4700,16 @@ dependencies = [
"windows-version",
]
[[package]]
name = "windows"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1de69df01bdf1ead2f4ac895dc77c9351aefff65b2f3db429a343f9cbf05e132"
dependencies = [
"windows-core 0.56.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows"
version = "0.58.0"
@ -4719,19 +4729,42 @@ dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4698e52ed2d08f8658ab0c39512a7c00ee5fe2688c65f8c0a4f06750d729f2a6"
dependencies = [
"windows-implement 0.56.0",
"windows-interface 0.56.0",
"windows-result 0.1.2",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement",
"windows-interface",
"windows-result",
"windows-implement 0.58.0",
"windows-interface 0.58.0",
"windows-result 0.2.0",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-implement"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6fc35f58ecd95a9b71c4f2329b911016e6bec66b3f2e6a4aad86bd2e99e2f9b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.68",
]
[[package]]
name = "windows-implement"
version = "0.58.0"
@ -4743,6 +4776,17 @@ dependencies = [
"syn 2.0.68",
]
[[package]]
name = "windows-interface"
version = "0.56.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08990546bf4edef8f431fa6326e032865f27138718c587dc21bc0265bbcb57cc"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.68",
]
[[package]]
name = "windows-interface"
version = "0.58.0"
@ -4760,11 +4804,20 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [
"windows-result",
"windows-result 0.2.0",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-result"
version = "0.2.0"
@ -4780,7 +4833,7 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result",
"windows-result 0.2.0",
"windows-targets 0.52.6",
]
@ -5078,7 +5131,7 @@ dependencies = [
"webkit2gtk",
"webkit2gtk-sys",
"webview2-com",
"windows",
"windows 0.58.0",
"windows-core 0.58.0",
"windows-version",
"x11-dl",

View File

@ -26,7 +26,7 @@ platform-dirs = "0.3.0"
once_cell = "1.19.0"
rosc = "~0.10"
regex = "1"
windows-core = "0.58.0"
windows-core = "0.56.0"
tracing = "0.1"
tracing-appender = "0.2"
tauri-plugin-shell = "2.0.0-rc"
@ -36,14 +36,38 @@ version = "0.3.16"
features = ["env-filter", "fmt", "json", "local-time", "time"]
[dependencies.windows]
version = "0.58.0"
version = "0.56.0"
features = [
"implement",
"Win32_System_Com",
"Win32_UI_Input_Ime",
"Win32_UI_TextServices",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_System_DataExchange",
"Win32_UI_WindowsAndMessaging"
"Win32_UI_WindowsAndMessaging",
"Foundation_Numerics",
"UI",
"Win32_Foundation",
"Win32_Globalization",
"Win32_Graphics_Direct2D",
"Win32_Graphics_Direct2D_Common",
"Win32_Graphics_DirectWrite",
"Win32_Graphics_Dxgi_Common",
"Win32_Graphics_Dwm",
"Win32_Graphics_Gdi",
"Win32_Graphics_Imaging",
"Win32_Security",
"Win32_System_Com",
"Win32_System_Console",
"Win32_System_Diagnostics_Debug",
"Win32_System_LibraryLoader",
"Win32_System_Ole",
"Win32_System_SystemServices",
"Win32_System_Registry",
"Win32_System_Variant",
"Win32_System_WindowsProgramming",
"Win32_UI_Controls",
"Win32_UI_HiDpi"
]

View File

@ -47,7 +47,7 @@ impl ConversionHandler {
impl ConversionHandler {
fn clipboard_has_owner(&mut self) -> bool {
unsafe { GetClipboardOwner() }.is_ok()
unsafe { GetClipboardOwner() }.0 != 0
}
fn tsf_conversion(&mut self, contents: &str, config: &Config) -> Result<()> {

View File

@ -10,6 +10,7 @@ mod handler;
mod transform_rule;
mod tsf;
mod tsf_conversion;
mod tauri_emit_subscriber;
use std::sync::Mutex;
@ -21,7 +22,9 @@ use clipboard_master::Master;
use com::Com;
use config::Config;
use handler::ConversionHandler;
use tracing::Level;
use tauri_emit_subscriber::TauriEmitSubscriber;
use tracing::{instrument::WithSubscriber, level_filters::LevelFilter, Level};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[derive(Serialize, Deserialize, Debug, Clone)]
struct Log {
@ -56,9 +59,7 @@ fn save_settings(config: Config, state: State<AppState>) -> Result<(), String> {
fn main() {
println!("VRClipboard-IME Logs\nバグがあった場合はこのログを送ってください。");
tracing_subscriber::fmt()
.with_max_level(Level::TRACE)
.init();
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
.plugin(tauri_plugin_updater::Builder::new().build())
@ -74,6 +75,11 @@ fn main() {
app.manage(STATE.lock().unwrap().clone());
let app_handle = app.app_handle().clone();
let registry = tracing_subscriber::registry().with(TauriEmitSubscriber {
app_handle: app_handle.clone(),
});
registry.init();
std::thread::spawn(move || {
let _com = Com::new().unwrap();

View File

@ -0,0 +1,66 @@
use chrono::{Datelike, Timelike};
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Emitter};
use tracing::Subscriber;
use tracing_subscriber::{registry::LookupSpan, Layer};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Event {
level: String,
message: String,
module_path: String,
timestamp: String,
}
pub struct TauriEmitSubscriber {
pub app_handle: AppHandle,
}
struct MessageExtractVisitor {
message: String,
module_path: String,
}
impl tracing::field::Visit for MessageExtractVisitor {
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
if field.name() == "message" {
self.message = format!("{:?}", value);
} else if field.name() == "log.module_path" {
self.module_path = format!("{:?}", value);
}
}
}
impl<S> Layer<S> for TauriEmitSubscriber
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
fn on_event(&self, event: &tracing::Event<'_>, _ctx: tracing_subscriber::layer::Context<'_, S>) {
let mut visitor = MessageExtractVisitor {
message: String::new(),
module_path: String::new()
};
event.record(&mut visitor);
let now = chrono::Local::now();
let event = Event {
level: event.metadata().level().to_string(),
message: visitor.message,
module_path: format!("{}{}", event.metadata().module_path().unwrap_or_default(), visitor.module_path),
timestamp: format!("{}-{}-{} {}:{}:{}", now.year(), now.month(), now.day(), now.hour(), now.minute(), now.second()),
};
if self
.app_handle
.emit(
"log-event",
event
)
.is_err()
{
println!("App handle add log failed");
}
}
}

View File

@ -3,8 +3,8 @@ use tracing::{debug, error, info};
use windows::Win32::{
System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER},
UI::{
Input::KeyboardAndMouse::HKL,
TextServices::{
HKL,
CLSID_TF_InputProcessorProfiles, ITfInputProcessorProfileMgr, GUID_TFCAT_TIP_KEYBOARD,
TF_INPUTPROCESSORPROFILE, TF_IPPMF_DONTCARECURRENTINPUTLANGUAGE,
TF_PROFILETYPE_INPUTPROCESSOR,

View File

@ -6,7 +6,7 @@ use windows::Win32::{
};
pub struct ThreadMgr {
thread_mgr: ITfThreadMgr2,
pub thread_mgr: ITfThreadMgr2,
}
impl ThreadMgr {

103
src/AboutComponent.tsx Normal file
View File

@ -0,0 +1,103 @@
import React from 'react';
import { Github, ExternalLink, Coffee } from 'lucide-react';
const AboutComponent: React.FC = () => {
const openLink = (url: string) => {
// ブラウザの標準APIを使用してリンクを開く
window.open(url, '_blank', 'noopener,noreferrer');
};
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">
<Coffee size={16} className="mr-1.5" />
</h2>
<div className="bg-white dark:bg-gray-800 rounded border border-gray-100 dark:border-gray-700 p-4 transition-colors">
<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
</div>
</div>
<div className="space-y-4">
<div>
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 border-b dark:border-gray-700 pb-1">
</h3>
<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.10.0</span>
</div>
<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">MIT</span>
</div>
<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">20252</span>
</div>
<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">Tauri, Rust, React, TypeScript</span>
</div>
</div>
</div>
<div>
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 border-b dark:border-gray-700 pb-1">
</h3>
<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">mii443</span>
</div>
<div className="flex items-center mb-1">
<span className="text-gray-700 dark:text-gray-300 w-20">VRChat:</span>
<span className="text-gray-600 dark:text-gray-400"> mii</span>
</div>
<div className="flex items-center mb-1">
<span className="text-gray-700 dark:text-gray-300 w-20">GitHub:</span>
<button
onClick={() => openLink('https://github.com/mii443')}
className="text-indigo-600 dark:text-indigo-400 hover:underline flex items-center"
>
mii443
<ExternalLink size={12} className="ml-1" />
</button>
</div>
</div>
</div>
<div>
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 border-b dark:border-gray-700 pb-1">
</h3>
<div className="flex flex-wrap gap-2">
<button
onClick={() => openLink('https://github.com/mii443/vrclipboard-ime-gui')}
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"
>
<Github size={14} className="mr-1.5" />
GitHubリポジトリ
</button>
<button
onClick={() => openLink('https://vrime.mii.dev')}
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"
>
<ExternalLink size={14} className="mr-1.5" />
</button>
</div>
</div>
</div>
</div>
</div>
);
};
export default AboutComponent;

View File

@ -2,7 +2,63 @@
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--scrollbar-thumb: #cbd5e1;
--scrollbar-track: transparent;
}
.dark {
--scrollbar-thumb: #4b5563;
--scrollbar-track: transparent;
}
html {
@apply antialiased text-sm;
}
body {
@apply overflow-hidden;
}
/* スクロールバーのカスタマイズ */
::-webkit-scrollbar {
@apply w-1.5;
}
::-webkit-scrollbar-track {
@apply bg-transparent;
}
::-webkit-scrollbar-thumb {
@apply bg-slate-200 hover:bg-slate-300 dark:bg-gray-600 dark:hover:bg-gray-500 transition-colors;
}
}
[data-tauri-drag-region] {
cursor: move;
user-select: none;
}
@layer components {
.input-field {
@apply w-full p-1.5 text-sm border rounded focus:border-indigo-400 dark:focus:border-indigo-500 outline-none transition-colors
dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200;
}
.btn {
@apply px-3 py-1 rounded text-sm font-medium focus:outline-none;
}
.btn-primary {
@apply bg-indigo-500 text-white hover:bg-indigo-600 dark:bg-indigo-600 dark:hover:bg-indigo-700;
}
.card {
@apply bg-white dark:bg-gray-800 rounded border border-gray-100 dark:border-gray-700 p-3 transition-colors;
}
.card-title {
@apply text-sm font-medium mb-2 text-gray-700 dark:text-gray-300 border-b dark:border-gray-700 pb-1 transition-colors;
}
}

View File

@ -1,9 +1,12 @@
import { useState, useEffect } from "react";
import { List, Settings } from 'lucide-react';
import { List, Settings, Terminal, Bug, Info } from 'lucide-react';
import { listen } from "@tauri-apps/api/event";
import "./App.css";
import TitleBar from "./TitleBar";
import SettingsComponent from "./SettingsComponent";
import { ThemeProvider, useTheme } from "./ThemeContext";
import TerminalComponent from "./TerminalComponent";
import AboutComponent from "./AboutComponent";
interface Log {
time: string;
@ -11,7 +14,8 @@ interface Log {
converted: string;
}
function App() {
const AppContent = () => {
const { theme } = useTheme();
const [logs, setLogs] = useState<Log[]>([]);
const [activeMenuItem, setActiveMenuItem] = useState('home');
@ -26,50 +30,84 @@ function App() {
}, []);
const renderLogEntry = (log: { time: string; original: string; converted: string }, index: number) => (
<div key={index} className="text-sm mb-1">
<span className="text-blue-600 font-medium">{log.time}</span>:
<span className="text-red-500 ml-2">{log.original}</span>
<span className="text-gray-500 mx-1"></span>
<span className="text-green-600">{log.converted}</span>
<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="flex flex-col sm:flex-row sm:items-center gap-1">
<div className="text-gray-800 dark:text-gray-200 px-1.5 py-0.5 bg-gray-50 dark:bg-gray-700 rounded flex-grow transition-colors">{log.original}</div>
<div className="text-gray-400 hidden sm:block text-xs"></div>
<div className="text-emerald-600 dark:text-emerald-400 px-1.5 py-0.5 bg-emerald-50 dark:bg-emerald-900/30 rounded flex-grow transition-colors">{log.converted}</div>
</div>
</div>
);
const MenuItem = ({ icon, label, id }: { icon: React.ReactNode, label: string, id: string }) => (
<button
onClick={() => setActiveMenuItem(id)}
className={`flex items-center w-full px-3 py-2 rounded transition-colors ${
activeMenuItem === id
? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/50 dark:text-indigo-300'
: 'hover:bg-gray-100 text-gray-600 dark:hover:bg-gray-700 dark:text-gray-300'
}`}
>
<div className="flex items-center justify-center w-5 h-5">
{icon}
</div>
<span className="ml-2 text-sm">{label}</span>
</button>
);
const renderContent = () => {
switch (activeMenuItem) {
case 'home':
return (
<div>
<h2 className="text-lg font-semibold mb-2"></h2>
<div className="bg-white p-3 rounded-md shadow-inner h-[calc(100vh-100px)] overflow-y-auto">
{logs.map((log, index) => renderLogEntry(log, index))}
<div className="h-full">
<h2 className="text-base font-medium mb-2 text-gray-700 dark:text-gray-200 flex items-center transition-colors">
<Terminal size={16} className="mr-1.5" />
</h2>
<div className="bg-gray-50 dark:bg-gray-800 border border-gray-100 dark:border-gray-700 p-2 rounded h-[calc(100vh-110px)] overflow-y-auto transition-colors">
{logs.length > 0 ? (
logs.map((log, index) => renderLogEntry(log, index))
) : (
<div className="flex flex-col items-center justify-center h-full text-gray-400 dark:text-gray-500">
<Terminal size={24} strokeWidth={1.5} />
<p className="mt-2 text-sm"></p>
<p className="text-xs"></p>
</div>
)}
</div>
</div>
);
case 'settings':
return <SettingsComponent />;
case 'terminal':
return <TerminalComponent />;
case 'about':
return <AboutComponent />;
default:
return null;
}
};
return (
<div>
<div className="h-screen flex flex-col bg-white dark:bg-gray-900 transition-colors">
<TitleBar />
<div className="flex h-[calc(100vh-32px)] bg-gray-100">
<div className="flex flex-1 h-[calc(100vh-32px)] overflow-hidden">
{/* サイドメニュー */}
<div className="w-16 bg-gray-800 text-white p-4" data-tauri-drag-region>
<div className="flex flex-col items-center space-y-4">
<button onClick={() => setActiveMenuItem('home')} className={`p-2 rounded ${activeMenuItem === 'home' ? 'bg-gray-600' : ''}`}>
<List size={24} />
</button>
<button onClick={() => setActiveMenuItem('settings')} className={`p-2 rounded ${activeMenuItem === 'settings' ? 'bg-gray-600' : ''}`}>
<Settings size={24} />
</button>
<div className="w-36 py-2 px-1 border-r border-gray-100 dark:border-gray-800 bg-white dark:bg-gray-800 transition-colors flex flex-col h-full">
<div className="space-y-1 flex-grow">
<MenuItem icon={<List size={16} />} label="ログ" id="home" />
<MenuItem icon={<Settings size={16} />} label="設定" id="settings" />
</div>
{/* 下側にデバッグタブを配置 */}
<div className="pt-2 border-t border-gray-100 dark:border-gray-700">
<MenuItem icon={<Bug size={16} />} label="デバッグ" id="terminal" />
<MenuItem icon={<Info size={16} />} label="情報" id="about" />
</div>
</div>
{/* メインコンテンツ */}
<div className="flex-1 p-4 overflow-y-auto">
<div className="flex-1 p-3 overflow-y-auto dark:text-gray-200 transition-colors">
{renderContent()}
</div>
</div>
@ -77,4 +115,12 @@ function App() {
);
};
export default App;
function App() {
return (
<ThemeProvider>
<AppContent />
</ThemeProvider>
);
}
export default App;

View File

@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { ChevronDown } from 'lucide-react';
import { ChevronDown, Settings, Save, AlertCircle, Check } from 'lucide-react';
interface Config {
prefix: string;
@ -19,7 +19,69 @@ enum OnCopyMode {
SendDirectly = 'SendDirectly'
}
const SettingsComponent = () => {
interface InputFieldProps {
name: string;
label: string;
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
disabled?: boolean;
description?: string;
}
const InputField: React.FC<InputFieldProps> = ({ name, label, value, onChange, disabled, description }) => (
<div className="mb-3">
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 transition-colors">
{label}
</label>
<input
type="text"
name={name}
value={value}
onChange={onChange}
className={`w-full p-1.5 text-sm border rounded focus:border-indigo-400 outline-none transition-colors ${
disabled
? 'bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 cursor-not-allowed'
: 'bg-white dark:bg-gray-700 dark:text-gray-200 dark:border-gray-600'
}`}
disabled={disabled}
/>
{description && (
<p className="mt-0.5 text-xs text-gray-500 dark:text-gray-400 transition-colors">{description}</p>
)}
</div>
);
interface CheckboxFieldProps {
id: string;
name: string;
label: React.ReactNode;
checked: boolean;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
const CheckboxField: React.FC<CheckboxFieldProps> = ({ id, name, label, checked, onChange }) => (
<div className="mb-2">
<div className="flex items-start">
<div className="flex items-center h-4">
<input
type="checkbox"
id={id}
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"
/>
</div>
<div className="ml-2 text-xs">
<label htmlFor={id} className="text-gray-700 dark:text-gray-300 transition-colors">
{label}
</label>
</div>
</div>
</div>
);
const SettingsComponent: React.FC = () => {
const [settings, setSettings] = useState<Config>({
prefix: ';',
split: '/',
@ -31,6 +93,7 @@ const SettingsComponent = () => {
skip_on_out_of_vrc: true,
});
const [isOpen, setIsOpen] = useState(false);
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle');
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@ -51,12 +114,15 @@ const SettingsComponent = () => {
};
const saveSettings = async (newSettings: Config) => {
setSaveStatus('saving');
try {
await invoke('save_settings', { config: newSettings });
alert('設定が正常に保存されました。');
setSaveStatus('success');
setTimeout(() => setSaveStatus('idle'), 2000);
} catch (error) {
console.error('Failed to save settings:', error);
alert('設定の保存に失敗しました。もう一度お試しください。');
setSaveStatus('error');
setTimeout(() => setSaveStatus('idle'), 3000);
}
};
@ -94,125 +160,134 @@ const SettingsComponent = () => {
}
};
const SaveStatusIndicator = () => {
switch (saveStatus) {
case 'saving':
return <span className="flex items-center text-indigo-500 text-xs animate-pulse"><Save size={10} className="mr-0.5" /> </span>;
case 'success':
return <span className="flex items-center text-green-500 text-xs"><Check size={10} className="mr-0.5" /> </span>;
case 'error':
return <span className="flex items-center text-red-500 text-xs"><AlertCircle size={10} className="mr-0.5" /> </span>;
default:
return null;
}
};
return (
<div>
<h2 className="text-lg font-semibold mb-2"></h2>
<div className="bg-white p-3 rounded-md shadow-inner h-[calc(100vh-100px)] overflow-y-auto">
<div className="space-y-4">
<div className="h-full">
<div className="flex items-center justify-between mb-2">
<h2 className="text-base font-medium text-gray-700 dark:text-gray-200 flex items-center transition-colors">
<Settings size={16} className="mr-1.5" />
</h2>
<SaveStatusIndicator />
</div>
<div className="bg-white dark:bg-gray-800 rounded border border-gray-100 dark:border-gray-700 p-3 transition-colors">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<input
type="text"
<h3 className="text-sm font-medium mb-2 text-gray-700 dark:text-gray-300 border-b dark:border-gray-700 pb-1 transition-colors"></h3>
<InputField
name="split"
label="区切り文字"
value={settings.split}
onChange={handleChange}
className="w-full p-2 border rounded-md"
description="複数の変換モードを使いたい場合の区切り文字"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<input
type="text"
<InputField
name="command"
label="モード変更文字"
value={settings.command}
onChange={handleChange}
className="w-full p-2 border rounded-md"
description="変換モードを変更するための文字"
/>
</div>
<div className="flex items-center">
<input
type="checkbox"
<CheckboxField
id="ignore_prefix"
name="ignore_prefix"
label="無条件で変換"
checked={settings.ignore_prefix}
onChange={handleChange}
className="mr-2"
/>
<label htmlFor="ignore_prefix" className="text-sm font-medium text-gray-700">
</label>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<input
type="text"
<InputField
name="prefix"
label="開始文字"
value={settings.prefix}
onChange={handleChange}
className={`w-full p-2 border rounded-md ${settings.ignore_prefix ? 'bg-gray-100' : ''}`}
disabled={settings.ignore_prefix}
description="変換を開始する文字(無条件で変換がオンの場合は無効)"
/>
<div className="relative mb-3" ref={dropdownRef}>
<label className="block text-xs font-medium text-gray-700 dark:text-gray-300 mb-1 transition-colors">
</label>
<div
className="w-full p-1.5 text-sm border rounded bg-white dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200 flex justify-between items-center cursor-pointer hover:border-indigo-300 dark:hover:border-indigo-500 transition-colors"
onClick={() => setIsOpen(!isOpen)}
>
<span>{getOnCopyModeLabel(settings.on_copy_mode)}</span>
<ChevronDown size={14} className={`transition-transform ${isOpen ? 'transform rotate-180' : ''}`} />
</div>
{isOpen && (
<div className="absolute z-10 mt-0.5 w-full bg-white dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded shadow-sm overflow-hidden text-sm transition-colors">
{Object.values(OnCopyMode).map((mode) => (
<div
key={mode}
className="p-1.5 hover:bg-indigo-50 dark:hover:bg-indigo-900/50 cursor-pointer"
onClick={() => handleSelectChange(mode)}
>
<div className={`flex items-center ${settings.on_copy_mode === mode ? 'text-indigo-600 dark:text-indigo-400 font-medium' : 'dark:text-gray-300'}`}>
{settings.on_copy_mode === mode && <Check size={12} className="mr-1.5" />}
<span className={settings.on_copy_mode === mode ? 'ml-0' : 'ml-4'}>
{getOnCopyModeLabel(mode)}
</span>
</div>
</div>
))}
</div>
)}
</div>
</div>
<div className="flex items-center">
<input
type="checkbox"
<div>
<h3 className="text-sm font-medium mb-2 text-gray-700 dark:text-gray-300 border-b dark:border-gray-700 pb-1 transition-colors"></h3>
<CheckboxField
id="skip_url"
name="skip_url"
label="URL が含まれている文章をスキップ"
checked={settings.skip_url}
onChange={handleChange}
className="mr-2"
/>
<label htmlFor="skip_url" className="text-sm font-medium text-gray-700">
URL
</label>
</div>
<div className="relative" ref={dropdownRef}>
<label className="block text-sm font-medium text-gray-700 mb-1">
</label>
<div
className="w-full p-2 border rounded-md bg-white flex justify-between items-center cursor-pointer"
onClick={() => setIsOpen(!isOpen)}
>
<span>{getOnCopyModeLabel(settings.on_copy_mode)}</span>
<ChevronDown className={`transition-transform duration-200 ${isOpen ? 'transform rotate-180' : ''}`} />
</div>
{isOpen && (
<div className="absolute z-10 mt-1 w-full bg-white border border-gray-300 rounded-md shadow-lg">
{Object.values(OnCopyMode).map((mode) => (
<div
key={mode}
className="p-2 hover:bg-gray-100 cursor-pointer"
onClick={() => handleSelectChange(mode)}
>
{getOnCopyModeLabel(mode)}
</div>
))}
</div>
)}
</div>
<div className="flex items-center">
<input
type="checkbox"
id="use_tsf_reconvert"
name="use_tsf_reconvert"
checked={settings.use_tsf_reconvert}
onChange={handleChange}
className="mr-2"
/>
<label htmlFor="use_tsf_reconvert" className="text-sm font-medium text-gray-700">
ベータ機能: Text Services Framework 使<br />
Windows10または11を使用している場合は Microsoft IME 使
</label>
</div>
<div className="flex items-center">
<input
type="checkbox"
<CheckboxField
id="skip_on_out_of_vrc"
name="skip_on_out_of_vrc"
label="VRChat以外からのコピーをスキップ"
checked={settings.skip_on_out_of_vrc}
onChange={handleChange}
className="mr-2"
/>
<label htmlFor="skip_on_out_of_vrc" className="text-sm font-medium text-gray-700">
VRChat以外からのコピーをスキップ
</label>
<div className="mb-3 p-2 bg-gray-50 dark:bg-gray-700 rounded border border-gray-200 dark:border-gray-600 text-xs transition-colors">
<CheckboxField
id="use_tsf_reconvert"
name="use_tsf_reconvert"
label={
<span>
<span className="text-indigo-600 dark:text-indigo-400 font-medium transition-colors">:</span> TSF再変換を使用
</span>
}
checked={settings.use_tsf_reconvert}
onChange={handleChange}
/>
<p className="text-xs text-gray-500 dark:text-gray-400 mt-0.5 ml-5 transition-colors">
Windows10/11 Microsoft IME 使
</p>
</div>
</div>
</div>
</div>

175
src/TerminalComponent.tsx Normal file
View File

@ -0,0 +1,175 @@
import React, { useState, useEffect, useRef } from 'react';
import { listen } from '@tauri-apps/api/event';
import { Check, Copy, Trash, Download } from 'lucide-react';
interface LogMessage {
level: 'info' | 'warn' | 'error' | 'debug' | 'trace';
message: string;
module_path: string;
timestamp: string;
}
const TerminalComponent: React.FC = () => {
const [logs, setLogs] = useState<LogMessage[]>([]);
const [filter, setFilter] = useState<string>('');
const [autoScroll, setAutoScroll] = useState<boolean>(true);
const [copied, setCopied] = useState<boolean>(false);
const terminalRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const setupLogListener = async () => {
try {
const unlisten = await listen<LogMessage>('log-event', (event) => {
setLogs(prev => [...prev, event.payload]);
if (autoScroll && terminalRef.current) {
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
}
});
return () => {
unlisten();
};
} catch (error) {
console.error('Failed to set up log listener:', error);
}
};
setupLogListener();
if (terminalRef.current) {
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
}
}, []);
useEffect(() => {
if (autoScroll && terminalRef.current) {
terminalRef.current.scrollTop = terminalRef.current.scrollHeight;
}
}, [logs, filter, autoScroll]);
const getLogColor = (level: string): string => {
switch (level) {
case 'ERROR': return 'text-red-500 dark:text-red-400';
case 'WARN': return 'text-yellow-600 dark:text-yellow-400';
case 'INFO': return 'text-blue-500 dark:text-blue-400';
case 'DEBUG': return 'text-purple-500 dark:text-purple-400';
case 'TRACE': return 'text-gray-500 dark:text-gray-400';
default: return 'text-gray-700 dark:text-gray-300';
}
};
const handleCopyLogs = async () => {
const logText = logs
.filter(log => filter === '' || log.message.toLowerCase().includes(filter.toLowerCase()) || log.level.includes(filter.toLowerCase()))
.map(log => `[${log.timestamp} ${log.module_path}] [${log.level.toUpperCase()}] ${log.message}`)
.join('\n');
try {
await navigator.clipboard.writeText(logText);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch (error) {
console.error('Failed to copy logs:', error);
}
};
const handleClearLogs = () => {
setLogs([]);
};
const handleSaveLogs = () => {
const logText = logs
.filter(log => filter === '' || log.message.toLowerCase().includes(filter.toLowerCase()) || log.level.includes(filter.toLowerCase()))
.map(log => `[${log.timestamp} ${log.module_path}] [${log.level.toUpperCase()}] ${log.message}`)
.join('\n');
const blob = new Blob([logText], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'vrclipboard-ime-debug.log';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
const filteredLogs = logs.filter(
log => filter === '' ||
log.message.toLowerCase().includes(filter.toLowerCase()) ||
log.level.includes(filter.toLowerCase())
);
return (
<div className="h-full">
<h2 className="text-base font-medium mb-2 text-gray-700 dark:text-gray-200 flex items-center justify-between">
<span></span>
<div className="flex items-center space-x-2">
<button
onClick={handleCopyLogs}
className="flex items-center text-xs text-gray-600 dark:text-gray-400 hover:text-indigo-600 dark:hover:text-indigo-400 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded"
title="ログをコピー"
>
{copied ? <Check size={12} className="mr-1" /> : <Copy size={12} className="mr-1" />}
{copied ? 'コピー完了' : 'コピー'}
</button>
<button
onClick={handleClearLogs}
className="flex items-center text-xs text-gray-600 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded"
title="ログをクリア"
>
<Trash size={12} className="mr-1" />
</button>
<button
onClick={handleSaveLogs}
className="flex items-center text-xs text-gray-600 dark:text-gray-400 hover:text-green-600 dark:hover:text-green-400 bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded"
title="ログを保存"
>
<Download size={12} className="mr-1" />
</button>
</div>
</h2>
<div className="flex items-center mb-2 space-x-2">
<input
type="text"
placeholder="ログをフィルタ..."
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="text-xs p-1.5 w-full border rounded bg-white dark:bg-gray-700 dark:border-gray-600 dark:text-gray-200"
/>
<div className="flex items-center">
<input
type="checkbox"
id="auto-scroll"
checked={autoScroll}
onChange={() => setAutoScroll(!autoScroll)}
className="mr-1 h-3 w-3"
/>
<label htmlFor="auto-scroll" className="text-xs text-gray-600 dark:text-gray-400"></label>
</div>
</div>
<div
ref={terminalRef}
className="font-mono text-xs bg-black dark:bg-gray-900 text-green-400 p-2 rounded border border-gray-700 h-[calc(100vh-150px)] overflow-y-auto"
>
{filteredLogs.length > 0 ? (
filteredLogs.map((log, index) => (
<div key={index} className={`mb-1 ${getLogColor(log.level)}`}>
<span className="text-gray-500 dark:text-gray-400">[{log.timestamp} {log.module_path}]</span>{' '}
<span className="font-semibold">[{log.level.toUpperCase()}]</span>{' '}
<span>{log.message}</span>
</div>
))
) : (
<div className="text-gray-500 dark:text-gray-400 italic"></div>
)}
</div>
</div>
);
};
export default TerminalComponent;

51
src/ThemeContext.tsx Normal file
View File

@ -0,0 +1,51 @@
import React, { createContext, useState, useContext, useEffect } from 'react';
type Theme = 'light' | 'dark';
interface ThemeContextType {
theme: Theme;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
// ローカルストレージからテーマ設定を取得するか、システム設定に基づいて初期テーマを設定
const [theme, setTheme] = useState<Theme>(() => {
const savedTheme = localStorage.getItem('theme') as Theme;
if (savedTheme) {
return savedTheme;
}
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
});
// テーマが変更されたときにHTMLのクラスとローカルストレージを更新
useEffect(() => {
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
localStorage.setItem('theme', theme);
}, [theme]);
// テーマの切り替え関数
const toggleTheme = () => {
setTheme(prevTheme => prevTheme === 'light' ? 'dark' : 'light');
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
// カスタムフック
export const useTheme = (): ThemeContextType => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};

View File

@ -1,28 +1,110 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { X, Minus, Square } from 'lucide-react';
const appWindow = getCurrentWebviewWindow()
import { listen } from '@tauri-apps/api/event';
import { X, Minus, Square, Maximize2, Moon, Sun } from 'lucide-react';
import { useState, useEffect } from 'react';
import { useTheme } from './ThemeContext';
const appWindow = getCurrentWebviewWindow();
const TitleBar = () => {
const [isMaximized, setIsMaximized] = useState(false);
const { theme, toggleTheme } = useTheme();
useEffect(() => {
const checkMaximized = async () => {
try {
const maximized = await appWindow.isMaximized();
setIsMaximized(maximized);
} catch (error) {
console.error('Failed to check if window is maximized:', error);
}
};
checkMaximized();
// Tauriのイベントリスナーを設定
let unlistenMaximize: (() => void) | undefined;
let unlistenRestore: (() => void) | undefined;
const setupListeners = async () => {
try {
// ウィンドウ最大化イベントをリッスン
unlistenMaximize = await listen('tauri://resize', () => {
checkMaximized();
});
// ウィンドウ復元イベントもリッスン
unlistenRestore = await listen('tauri://move', () => {
checkMaximized();
});
} catch (error) {
console.error('Failed to set up event listeners:', error);
}
};
setupListeners();
return () => {
// クリーンアップ
if (unlistenMaximize) unlistenMaximize();
if (unlistenRestore) unlistenRestore();
};
}, []);
const handleClose = () => appWindow.close();
const handleMinimize = () => appWindow.minimize();
const handleToggleMaximize = () => appWindow.toggleMaximize();
const TitleButton = ({
onClick,
icon,
hoverClass = "hover:bg-gray-700"
}: {
onClick: () => void,
icon: React.ReactNode,
hoverClass?: string
}) => (
<button
onClick={onClick}
className={`p-1 rounded focus:outline-none transition-colors ${hoverClass}`}
>
{icon}
</button>
);
return (
<div className="flex justify-between items-center bg-gray-800 text-white h-8 px-2" data-tauri-drag-region>
<div className="flex items-center">
<span className="text-sm font-semibold">VRClipboard-IME</span>
<span className="text-xs font-semibold ml-2">v1.10.0</span>
<div
className="flex justify-between items-center bg-indigo-600 dark:bg-indigo-900 text-white h-8 px-2 transition-colors"
data-tauri-drag-region
>
<div className="flex items-center" data-tauri-drag-region>
<span className="text-xs font-medium" data-tauri-drag-region>VRClipboard-IME</span>
<span className="text-xs opacity-80 ml-1" data-tauri-drag-region>v1.10.0</span>
</div>
<div className="flex">
<button onClick={handleMinimize} className="p-1 hover:bg-gray-700 focus:outline-none">
<Minus size={16} />
</button>
<button onClick={handleToggleMaximize} className="p-1 hover:bg-gray-700 focus:outline-none">
<Square size={16} />
</button>
<button onClick={handleClose} className="p-1 hover:bg-red-600 focus:outline-none">
<X size={16} />
</button>
<TitleButton
onClick={toggleTheme}
icon={theme === 'dark' ?
<Sun size={12} className="text-white/90" /> :
<Moon size={12} className="text-white/90" />
}
/>
<TitleButton
onClick={handleMinimize}
icon={<Minus size={12} className="text-white/90" />}
/>
<TitleButton
onClick={handleToggleMaximize}
icon={isMaximized ?
<Square size={12} className="text-white/90" /> :
<Maximize2 size={12} className="text-white/90" />
}
/>
<TitleButton
onClick={handleClose}
icon={<X size={14} className="text-white/90" />}
hoverClass="hover:bg-red-600"
/>
</div>
</div>
);

View File

@ -1,9 +1,9 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
darkMode: 'class',
theme: {
extend: {},
},
plugins: [],
}
}