mirror of
https://github.com/mii443/vrclipboard-ime-gui.git
synced 2025-12-03 03:08:27 +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.
|
# This file is automatically @generated by Cargo.
|
||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 4
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
@@ -3577,7 +3577,7 @@ dependencies = [
|
|||||||
"tao-macros",
|
"tao-macros",
|
||||||
"unicode-segmentation",
|
"unicode-segmentation",
|
||||||
"url",
|
"url",
|
||||||
"windows",
|
"windows 0.58.0",
|
||||||
"windows-core 0.58.0",
|
"windows-core 0.58.0",
|
||||||
"windows-version",
|
"windows-version",
|
||||||
"x11-dl",
|
"x11-dl",
|
||||||
@@ -3658,7 +3658,7 @@ dependencies = [
|
|||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"window-vibrancy",
|
"window-vibrancy",
|
||||||
"windows",
|
"windows 0.58.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3808,7 +3808,7 @@ dependencies = [
|
|||||||
"tauri-utils",
|
"tauri-utils",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"url",
|
"url",
|
||||||
"windows",
|
"windows 0.58.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3833,7 +3833,7 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows",
|
"windows 0.58.0",
|
||||||
"wry",
|
"wry",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4422,8 +4422,8 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
"tracing-appender",
|
"tracing-appender",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"windows",
|
"windows 0.56.0",
|
||||||
"windows-core 0.58.0",
|
"windows-core 0.56.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4627,10 +4627,10 @@ checksum = "6f61ff3d9d0ee4efcb461b14eb3acfda2702d10dc329f339303fc3e57215ae2c"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"webview2-com-macros",
|
"webview2-com-macros",
|
||||||
"webview2-com-sys",
|
"webview2-com-sys",
|
||||||
"windows",
|
"windows 0.58.0",
|
||||||
"windows-core 0.58.0",
|
"windows-core 0.58.0",
|
||||||
"windows-implement",
|
"windows-implement 0.58.0",
|
||||||
"windows-interface",
|
"windows-interface 0.58.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4651,7 +4651,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886"
|
checksum = "a3a3e2eeb58f82361c93f9777014668eb3d07e7d174ee4c819575a9208011886"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"windows",
|
"windows 0.58.0",
|
||||||
"windows-core 0.58.0",
|
"windows-core 0.58.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4700,6 +4700,16 @@ dependencies = [
|
|||||||
"windows-version",
|
"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]]
|
[[package]]
|
||||||
name = "windows"
|
name = "windows"
|
||||||
version = "0.58.0"
|
version = "0.58.0"
|
||||||
@@ -4719,19 +4729,42 @@ dependencies = [
|
|||||||
"windows-targets 0.52.6",
|
"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]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.58.0"
|
version = "0.58.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-implement",
|
"windows-implement 0.58.0",
|
||||||
"windows-interface",
|
"windows-interface 0.58.0",
|
||||||
"windows-result",
|
"windows-result 0.2.0",
|
||||||
"windows-strings",
|
"windows-strings",
|
||||||
"windows-targets 0.52.6",
|
"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]]
|
[[package]]
|
||||||
name = "windows-implement"
|
name = "windows-implement"
|
||||||
version = "0.58.0"
|
version = "0.58.0"
|
||||||
@@ -4743,6 +4776,17 @@ dependencies = [
|
|||||||
"syn 2.0.68",
|
"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]]
|
[[package]]
|
||||||
name = "windows-interface"
|
name = "windows-interface"
|
||||||
version = "0.58.0"
|
version = "0.58.0"
|
||||||
@@ -4760,11 +4804,20 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
|
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-result",
|
"windows-result 0.2.0",
|
||||||
"windows-strings",
|
"windows-strings",
|
||||||
"windows-targets 0.52.6",
|
"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]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -4780,7 +4833,7 @@ version = "0.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-result",
|
"windows-result 0.2.0",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.52.6",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5078,7 +5131,7 @@ dependencies = [
|
|||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
"webkit2gtk-sys",
|
"webkit2gtk-sys",
|
||||||
"webview2-com",
|
"webview2-com",
|
||||||
"windows",
|
"windows 0.58.0",
|
||||||
"windows-core 0.58.0",
|
"windows-core 0.58.0",
|
||||||
"windows-version",
|
"windows-version",
|
||||||
"x11-dl",
|
"x11-dl",
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ platform-dirs = "0.3.0"
|
|||||||
once_cell = "1.19.0"
|
once_cell = "1.19.0"
|
||||||
rosc = "~0.10"
|
rosc = "~0.10"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
windows-core = "0.58.0"
|
windows-core = "0.56.0"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-appender = "0.2"
|
tracing-appender = "0.2"
|
||||||
tauri-plugin-shell = "2.0.0-rc"
|
tauri-plugin-shell = "2.0.0-rc"
|
||||||
@@ -36,14 +36,38 @@ version = "0.3.16"
|
|||||||
features = ["env-filter", "fmt", "json", "local-time", "time"]
|
features = ["env-filter", "fmt", "json", "local-time", "time"]
|
||||||
|
|
||||||
[dependencies.windows]
|
[dependencies.windows]
|
||||||
version = "0.58.0"
|
version = "0.56.0"
|
||||||
features = [
|
features = [
|
||||||
|
"implement",
|
||||||
"Win32_System_Com",
|
"Win32_System_Com",
|
||||||
"Win32_UI_Input_Ime",
|
"Win32_UI_Input_Ime",
|
||||||
"Win32_UI_TextServices",
|
"Win32_UI_TextServices",
|
||||||
"Win32_UI_Input_KeyboardAndMouse",
|
"Win32_UI_Input_KeyboardAndMouse",
|
||||||
"Win32_System_DataExchange",
|
"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 {
|
impl ConversionHandler {
|
||||||
fn clipboard_has_owner(&mut self) -> bool {
|
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<()> {
|
fn tsf_conversion(&mut self, contents: &str, config: &Config) -> Result<()> {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ mod handler;
|
|||||||
mod transform_rule;
|
mod transform_rule;
|
||||||
mod tsf;
|
mod tsf;
|
||||||
mod tsf_conversion;
|
mod tsf_conversion;
|
||||||
|
mod tauri_emit_subscriber;
|
||||||
|
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
|
|
||||||
@@ -21,7 +22,9 @@ use clipboard_master::Master;
|
|||||||
use com::Com;
|
use com::Com;
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use handler::ConversionHandler;
|
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)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
struct Log {
|
struct Log {
|
||||||
@@ -56,9 +59,7 @@ fn save_settings(config: Config, state: State<AppState>) -> Result<(), String> {
|
|||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("VRClipboard-IME Logs\nバグがあった場合はこのログを送ってください。");
|
println!("VRClipboard-IME Logs\nバグがあった場合はこのログを送ってください。");
|
||||||
tracing_subscriber::fmt()
|
|
||||||
.with_max_level(Level::TRACE)
|
|
||||||
.init();
|
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_shell::init())
|
.plugin(tauri_plugin_shell::init())
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
@@ -74,6 +75,11 @@ fn main() {
|
|||||||
app.manage(STATE.lock().unwrap().clone());
|
app.manage(STATE.lock().unwrap().clone());
|
||||||
let app_handle = app.app_handle().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 || {
|
std::thread::spawn(move || {
|
||||||
let _com = Com::new().unwrap();
|
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::{
|
use windows::Win32::{
|
||||||
System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER},
|
System::Com::{CoCreateInstance, CLSCTX_INPROC_SERVER},
|
||||||
UI::{
|
UI::{
|
||||||
Input::KeyboardAndMouse::HKL,
|
|
||||||
TextServices::{
|
TextServices::{
|
||||||
|
HKL,
|
||||||
CLSID_TF_InputProcessorProfiles, ITfInputProcessorProfileMgr, GUID_TFCAT_TIP_KEYBOARD,
|
CLSID_TF_InputProcessorProfiles, ITfInputProcessorProfileMgr, GUID_TFCAT_TIP_KEYBOARD,
|
||||||
TF_INPUTPROCESSORPROFILE, TF_IPPMF_DONTCARECURRENTINPUTLANGUAGE,
|
TF_INPUTPROCESSORPROFILE, TF_IPPMF_DONTCARECURRENTINPUTLANGUAGE,
|
||||||
TF_PROFILETYPE_INPUTPROCESSOR,
|
TF_PROFILETYPE_INPUTPROCESSOR,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use windows::Win32::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub struct ThreadMgr {
|
pub struct ThreadMgr {
|
||||||
thread_mgr: ITfThreadMgr2,
|
pub thread_mgr: ITfThreadMgr2,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ThreadMgr {
|
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 components;
|
||||||
@tailwind utilities;
|
@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] {
|
[data-tauri-drag-region] {
|
||||||
cursor: move;
|
cursor: move;
|
||||||
user-select: none;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
90
src/App.tsx
90
src/App.tsx
@@ -1,9 +1,12 @@
|
|||||||
import { useState, useEffect } from "react";
|
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 { listen } from "@tauri-apps/api/event";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
import TitleBar from "./TitleBar";
|
import TitleBar from "./TitleBar";
|
||||||
import SettingsComponent from "./SettingsComponent";
|
import SettingsComponent from "./SettingsComponent";
|
||||||
|
import { ThemeProvider, useTheme } from "./ThemeContext";
|
||||||
|
import TerminalComponent from "./TerminalComponent";
|
||||||
|
import AboutComponent from "./AboutComponent";
|
||||||
|
|
||||||
interface Log {
|
interface Log {
|
||||||
time: string;
|
time: string;
|
||||||
@@ -11,7 +14,8 @@ interface Log {
|
|||||||
converted: string;
|
converted: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function App() {
|
const AppContent = () => {
|
||||||
|
const { theme } = useTheme();
|
||||||
const [logs, setLogs] = useState<Log[]>([]);
|
const [logs, setLogs] = useState<Log[]>([]);
|
||||||
const [activeMenuItem, setActiveMenuItem] = useState('home');
|
const [activeMenuItem, setActiveMenuItem] = useState('home');
|
||||||
|
|
||||||
@@ -26,50 +30,84 @@ function App() {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const renderLogEntry = (log: { time: string; original: string; converted: string }, index: number) => (
|
const renderLogEntry = (log: { time: string; original: string; converted: string }, index: number) => (
|
||||||
<div key={index} className="text-sm mb-1">
|
<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">
|
||||||
<span className="text-blue-600 font-medium">{log.time}</span>:
|
<div className="text-xs text-gray-500 dark:text-gray-400 mb-1">{log.time}</div>
|
||||||
<span className="text-red-500 ml-2">{log.original}</span>
|
<div className="flex flex-col sm:flex-row sm:items-center gap-1">
|
||||||
<span className="text-gray-500 mx-1">→</span>
|
<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>
|
||||||
<span className="text-green-600">{log.converted}</span>
|
<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>
|
</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 = () => {
|
const renderContent = () => {
|
||||||
switch (activeMenuItem) {
|
switch (activeMenuItem) {
|
||||||
case 'home':
|
case 'home':
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="h-full">
|
||||||
<h2 className="text-lg font-semibold mb-2">変換ログ</h2>
|
<h2 className="text-base font-medium mb-2 text-gray-700 dark:text-gray-200 flex items-center transition-colors">
|
||||||
<div className="bg-white p-3 rounded-md shadow-inner h-[calc(100vh-100px)] overflow-y-auto">
|
<Terminal size={16} className="mr-1.5" />
|
||||||
{logs.map((log, index) => renderLogEntry(log, index))}
|
変換ログ
|
||||||
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
case 'settings':
|
case 'settings':
|
||||||
return <SettingsComponent />;
|
return <SettingsComponent />;
|
||||||
|
case 'terminal':
|
||||||
|
return <TerminalComponent />;
|
||||||
|
case 'about':
|
||||||
|
return <AboutComponent />;
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="h-screen flex flex-col bg-white dark:bg-gray-900 transition-colors">
|
||||||
<TitleBar />
|
<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="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="flex flex-col items-center space-y-4">
|
<div className="space-y-1 flex-grow">
|
||||||
<button onClick={() => setActiveMenuItem('home')} className={`p-2 rounded ${activeMenuItem === 'home' ? 'bg-gray-600' : ''}`}>
|
<MenuItem icon={<List size={16} />} label="ログ" id="home" />
|
||||||
<List size={24} />
|
<MenuItem icon={<Settings size={16} />} label="設定" id="settings" />
|
||||||
</button>
|
</div>
|
||||||
<button onClick={() => setActiveMenuItem('settings')} className={`p-2 rounded ${activeMenuItem === 'settings' ? 'bg-gray-600' : ''}`}>
|
{/* 下側にデバッグタブを配置 */}
|
||||||
<Settings size={24} />
|
<div className="pt-2 border-t border-gray-100 dark:border-gray-700">
|
||||||
</button>
|
<MenuItem icon={<Bug size={16} />} label="デバッグ" id="terminal" />
|
||||||
|
<MenuItem icon={<Info size={16} />} label="情報" id="about" />
|
||||||
</div>
|
</div>
|
||||||
</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()}
|
{renderContent()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -77,4 +115,12 @@ function App() {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
|
<AppContent />
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default App;
|
export default App;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useState, useEffect, useRef } from 'react';
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { ChevronDown } from 'lucide-react';
|
import { ChevronDown, Settings, Save, AlertCircle, Check } from 'lucide-react';
|
||||||
|
|
||||||
interface Config {
|
interface Config {
|
||||||
prefix: string;
|
prefix: string;
|
||||||
@@ -19,7 +19,69 @@ enum OnCopyMode {
|
|||||||
SendDirectly = 'SendDirectly'
|
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>({
|
const [settings, setSettings] = useState<Config>({
|
||||||
prefix: ';',
|
prefix: ';',
|
||||||
split: '/',
|
split: '/',
|
||||||
@@ -31,6 +93,7 @@ const SettingsComponent = () => {
|
|||||||
skip_on_out_of_vrc: true,
|
skip_on_out_of_vrc: true,
|
||||||
});
|
});
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [saveStatus, setSaveStatus] = useState<'idle' | 'saving' | 'success' | 'error'>('idle');
|
||||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -51,12 +114,15 @@ const SettingsComponent = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const saveSettings = async (newSettings: Config) => {
|
const saveSettings = async (newSettings: Config) => {
|
||||||
|
setSaveStatus('saving');
|
||||||
try {
|
try {
|
||||||
await invoke('save_settings', { config: newSettings });
|
await invoke('save_settings', { config: newSettings });
|
||||||
alert('設定が正常に保存されました。');
|
setSaveStatus('success');
|
||||||
|
setTimeout(() => setSaveStatus('idle'), 2000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save settings:', 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 (
|
return (
|
||||||
<div>
|
<div className="h-full">
|
||||||
<h2 className="text-lg font-semibold mb-2">設定</h2>
|
<div className="flex items-center justify-between mb-2">
|
||||||
<div className="bg-white p-3 rounded-md shadow-inner h-[calc(100vh-100px)] overflow-y-auto">
|
<h2 className="text-base font-medium text-gray-700 dark:text-gray-200 flex items-center transition-colors">
|
||||||
<div className="space-y-4">
|
<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>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
<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>
|
||||||
区切り文字
|
|
||||||
</label>
|
<InputField
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="split"
|
name="split"
|
||||||
|
label="区切り文字"
|
||||||
value={settings.split}
|
value={settings.split}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full p-2 border rounded-md"
|
description="複数の変換モードを使いたい場合の区切り文字"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div>
|
<InputField
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
モード変更文字
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="command"
|
name="command"
|
||||||
|
label="モード変更文字"
|
||||||
value={settings.command}
|
value={settings.command}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="w-full p-2 border rounded-md"
|
description="変換モードを変更するための文字"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
<CheckboxField
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="ignore_prefix"
|
id="ignore_prefix"
|
||||||
name="ignore_prefix"
|
name="ignore_prefix"
|
||||||
|
label="無条件で変換"
|
||||||
checked={settings.ignore_prefix}
|
checked={settings.ignore_prefix}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="mr-2"
|
|
||||||
/>
|
/>
|
||||||
<label htmlFor="ignore_prefix" className="text-sm font-medium text-gray-700">
|
|
||||||
無条件で変換
|
<InputField
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
||||||
開始文字
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="prefix"
|
name="prefix"
|
||||||
|
label="開始文字"
|
||||||
value={settings.prefix}
|
value={settings.prefix}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className={`w-full p-2 border rounded-md ${settings.ignore_prefix ? 'bg-gray-100' : ''}`}
|
|
||||||
disabled={settings.ignore_prefix}
|
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>
|
||||||
<div className="flex items-center">
|
|
||||||
<input
|
<div>
|
||||||
type="checkbox"
|
<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"
|
id="skip_url"
|
||||||
name="skip_url"
|
name="skip_url"
|
||||||
|
label="URL が含まれている文章をスキップ"
|
||||||
checked={settings.skip_url}
|
checked={settings.skip_url}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="mr-2"
|
|
||||||
/>
|
/>
|
||||||
<label htmlFor="skip_url" className="text-sm font-medium text-gray-700">
|
|
||||||
URL が含まれている文章をスキップ
|
<CheckboxField
|
||||||
</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"
|
|
||||||
id="skip_on_out_of_vrc"
|
id="skip_on_out_of_vrc"
|
||||||
name="skip_on_out_of_vrc"
|
name="skip_on_out_of_vrc"
|
||||||
|
label="VRChat以外からのコピーをスキップ"
|
||||||
checked={settings.skip_on_out_of_vrc}
|
checked={settings.skip_on_out_of_vrc}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className="mr-2"
|
|
||||||
/>
|
/>
|
||||||
<label htmlFor="skip_on_out_of_vrc" className="text-sm font-medium text-gray-700">
|
|
||||||
VRChat以外からのコピーをスキップ
|
<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">
|
||||||
</label>
|
<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>
|
</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 { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||||
import { X, Minus, Square } from 'lucide-react';
|
import { listen } from '@tauri-apps/api/event';
|
||||||
const appWindow = getCurrentWebviewWindow()
|
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 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 handleClose = () => appWindow.close();
|
||||||
const handleMinimize = () => appWindow.minimize();
|
const handleMinimize = () => appWindow.minimize();
|
||||||
const handleToggleMaximize = () => appWindow.toggleMaximize();
|
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 (
|
return (
|
||||||
<div className="flex justify-between items-center bg-gray-800 text-white h-8 px-2" data-tauri-drag-region>
|
<div
|
||||||
<div className="flex items-center">
|
className="flex justify-between items-center bg-indigo-600 dark:bg-indigo-900 text-white h-8 px-2 transition-colors"
|
||||||
<span className="text-sm font-semibold">VRClipboard-IME</span>
|
data-tauri-drag-region
|
||||||
<span className="text-xs font-semibold ml-2">v1.10.0</span>
|
>
|
||||||
|
<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>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<button onClick={handleMinimize} className="p-1 hover:bg-gray-700 focus:outline-none">
|
<TitleButton
|
||||||
<Minus size={16} />
|
onClick={toggleTheme}
|
||||||
</button>
|
icon={theme === 'dark' ?
|
||||||
<button onClick={handleToggleMaximize} className="p-1 hover:bg-gray-700 focus:outline-none">
|
<Sun size={12} className="text-white/90" /> :
|
||||||
<Square size={16} />
|
<Moon size={12} className="text-white/90" />
|
||||||
</button>
|
}
|
||||||
<button onClick={handleClose} className="p-1 hover:bg-red-600 focus:outline-none">
|
/>
|
||||||
<X size={16} />
|
<TitleButton
|
||||||
</button>
|
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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
export default {
|
export default {
|
||||||
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
content: ["./src/**/*.{js,jsx,ts,tsx}"],
|
||||||
|
darkMode: 'class',
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user