mirror of
https://github.com/mii443/vrclipboard-ime-gui.git
synced 2025-08-22 08:05:32 +00:00
update UI
This commit is contained in:
87
src-tauri/Cargo.lock
generated
87
src-tauri/Cargo.lock
generated
@ -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",
|
||||
|
@ -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"
|
||||
]
|
||||
|
||||
|
||||
|
@ -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<()> {
|
||||
|
@ -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();
|
||||
|
||||
|
66
src-tauri/src/tauri_emit_subscriber.rs
Normal file
66
src-tauri/src/tauri_emit_subscriber.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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
103
src/AboutComponent.tsx
Normal 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">2025年2月</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;
|
56
src/App.css
56
src/App.css
@ -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;
|
||||
}
|
||||
}
|
92
src/App.tsx
92
src/App.tsx
@ -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;
|
@ -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
175
src/TerminalComponent.tsx
Normal 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
51
src/ThemeContext.tsx
Normal 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;
|
||||
};
|
112
src/TitleBar.tsx
112
src/TitleBar.tsx
@ -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>
|
||||
);
|
||||
|
@ -1,9 +1,9 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user