diff --git a/Cargo.lock b/Cargo.lock index 011312f0..5c7c59e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -59,6 +59,27 @@ version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.3", + "objc2-app-kit", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "wl-clipboard-rs", + "x11rb", +] + [[package]] name = "asn1-rs" version = "0.6.2" @@ -605,6 +626,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.10.1" @@ -782,6 +809,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-clipboard-manager", "tauri-plugin-devtools", "tauri-plugin-fs", "tauri-plugin-http", @@ -873,6 +901,15 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d" +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + [[package]] name = "cmake" version = "0.1.54" @@ -1414,6 +1451,12 @@ dependencies = [ "litrs", ] +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + [[package]] name = "dpi" version = "0.1.2" @@ -1548,6 +1591,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + [[package]] name = "event-listener" version = "2.5.3" @@ -1590,6 +1639,26 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "fax" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" +dependencies = [ + "fax_derive", +] + +[[package]] +name = "fax_derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.106", +] + [[package]] name = "fdeflate" version = "0.3.7" @@ -1615,6 +1684,12 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52051878f80a721bb68ebfbc930e07b65ba72f2da88968ea5c06fd6ca3d3a127" +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + [[package]] name = "flate2" version = "1.1.4" @@ -1947,6 +2022,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.2", + "windows-link 0.2.1", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -2525,7 +2610,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc50b891e4acf8fe0e71ef88ec43ad82ee07b3810ad09de10f1d01f072ed4b98" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] @@ -2641,6 +2726,20 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "529feb3e6769d234375c4cf1ee2ce713682b8e76538cb13f9fc23e1400a591e7" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.0", + "tiff", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2934,6 +3033,12 @@ version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -3166,6 +3271,16 @@ dependencies = [ "uuid", ] +[[package]] +name = "moxcms" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c588e11a3082784af229e23e8e4ecf5bcc6fbe4f69101e0421ce8d79da7f0b40" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "muda" version = "0.17.1" @@ -3181,7 +3296,7 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation 0.3.2", "once_cell", - "png", + "png 0.17.16", "serde", "thiserror 2.0.17", "windows-sys 0.60.2", @@ -3754,6 +3869,16 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_pipe" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "pango" version = "0.18.3" @@ -3830,6 +3955,16 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap 2.11.4", +] + [[package]] name = "phf" version = "0.8.0" @@ -4021,7 +4156,7 @@ checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" dependencies = [ "base64 0.22.1", "indexmap 2.11.4", - "quick-xml", + "quick-xml 0.38.3", "serde", "time", ] @@ -4067,6 +4202,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.9.4", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "2.8.0" @@ -4367,6 +4515,15 @@ dependencies = [ "unicase", ] +[[package]] +name = "pxfm" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3cbdf373972bf78df4d3b518d07003938e2c7d1fb5891e55f9cb6df57009d84" +dependencies = [ + "num-traits", +] + [[package]] name = "quanta" version = "0.11.1" @@ -4383,6 +4540,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.37.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +dependencies = [ + "memchr", +] + [[package]] name = "quick-xml" version = "0.38.3" @@ -4817,6 +4989,19 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.4", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.1.2" @@ -5756,7 +5941,7 @@ dependencies = [ "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", @@ -5803,6 +5988,21 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-clipboard-manager" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adddd9e9275b20e77af3061d100a25a884cced3c4c9ef680bd94dd0f7e26c1ca" +dependencies = [ + "arboard", + "log", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", +] + [[package]] name = "tauri-plugin-devtools" version = "2.0.1" @@ -6088,6 +6288,20 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "tiff" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af9605de7fee8d9551863fd692cce7637f548dbd9db9180fcc07ccc6d26c336f" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + [[package]] name = "time" version = "0.3.44" @@ -6643,12 +6857,24 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation 0.3.2", "once_cell", - "png", + "png 0.17.16", "serde", "thiserror 2.0.17", "windows-sys 0.59.0", ] +[[package]] +name = "tree_magic_mini" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f943391d896cdfe8eec03a04d7110332d445be7df856db382dd96a730667562c" +dependencies = [ + "memchr", + "nom", + "once_cell", + "petgraph", +] + [[package]] name = "triomphe" version = "0.1.15" @@ -7018,6 +7244,76 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wayland-backend" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.2", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +dependencies = [ + "bitflags 2.9.4", + "rustix 1.1.2", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" +dependencies = [ + "bitflags 2.9.4", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" +dependencies = [ + "bitflags 2.9.4", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +dependencies = [ + "proc-macro2", + "quick-xml 0.37.5", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +dependencies = [ + "pkg-config", +] + [[package]] name = "web-sys" version = "0.3.81" @@ -7161,6 +7457,12 @@ dependencies = [ "windows-core 0.61.2", ] +[[package]] +name = "weezl" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" + [[package]] name = "winapi" version = "0.3.9" @@ -7706,6 +8008,25 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "wl-clipboard-rs" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5ff8d0e60065f549fafd9d6cb626203ea64a798186c80d8e7df4f8af56baeb" +dependencies = [ + "libc", + "log", + "os_pipe", + "rustix 0.38.44", + "tempfile", + "thiserror 2.0.17", + "tree_magic_mini", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-wlr", +] + [[package]] name = "writeable" version = "0.6.1" @@ -7779,6 +8100,23 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "gethostname", + "rustix 1.1.2", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + [[package]] name = "x509-parser" version = "0.16.0" @@ -8016,6 +8354,21 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-jpeg" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ce2c8a9384ad323cf564b67da86e21d3cfdff87908bc1223ed5c99bc792713" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "5.8.0" diff --git a/crates/proxy_v2_models/src/data_type.rs b/crates/proxy_v2_models/src/data_type.rs index 09470538..1045eca6 100644 --- a/crates/proxy_v2_models/src/data_type.rs +++ b/crates/proxy_v2_models/src/data_type.rs @@ -223,34 +223,81 @@ pub fn detect_data_type(headers: &HeaderMap, body: &Bytes) -> DataType { // 바이너리 파일 내용 분석 (이미지, 동영상, 오디오, 문서, 아카이브만) - // 이미지 파일 감지 (구체적인 형식) + // 이미지 파일 감지 (구체적인 형식) - 우선순위 높음 // TODO @ohah: Improve image file detection logic if body.len() >= 2 { - // PNG 시그니처 + // PNG 시그니처 (매우 명확) if body.len() >= 8 && &body[0..8] == b"\x89PNG\r\n\x1a\n" { return DataType::Image; } - // JPEG 시그니처 + + // JPEG 시그니처 (매우 명확) if &body[0..2] == b"\xff\xd8" { return DataType::Image; } - // GIF 시그니처 + + // GIF 시그니처 (매우 명확) if body.len() >= 6 && (&body[0..6] == b"GIF87a" || &body[0..6] == b"GIF89a") { return DataType::Image; } - // WebP 시그니처 + + // WebP 시그니처 (RIFF 컨테이너 확인) if body.len() >= 12 && &body[0..4] == b"RIFF" && &body[8..12] == b"WEBP" { return DataType::Image; } + + // BMP 시그니처 + if body.len() >= 2 && &body[0..2] == b"BM" { + return DataType::Image; + } + + // ICO 시그니처 + if body.len() >= 4 && &body[0..4] == b"\x00\x00\x01\x00" { + return DataType::Image; + } + + // TIFF 시그니처 (Little Endian) + if body.len() >= 4 && &body[0..4] == b"II*\x00" { + return DataType::Image; + } + + // TIFF 시그니처 (Big Endian) + if body.len() >= 4 && &body[0..4] == b"MM\x00*" { + return DataType::Image; + } } // 비디오 파일 감지 (통합) // TODO @ohah: Improve video file detection logic - if body.len() >= 4 { - // MP4 시그니처 - if body.len() >= 8 && (&body[4..8] == b"ftyp" || &body[4..8] == b"moov") { + if body.len() >= 8 { + // MP4 시그니처 - 더 정확한 감지 + // MP4 파일은 4바이트 크기 + "ftyp" + 브랜드 식별자로 시작 + if &body[4..8] == b"ftyp" { + // MP4 브랜드 식별자 확인 (8-12번째 바이트) + if body.len() >= 12 { + let brand = &body[8..12]; + // 일반적인 MP4 브랜드들 + if brand == b"mp41" + || brand == b"mp42" + || brand == b"isom" + || brand == b"avc1" + || brand == b"iso2" + || brand == b"iso3" + || brand == b"iso4" + || brand == b"iso5" + || brand == b"iso6" + { + return DataType::Video; + } + } + } + // MOV 파일 시그니처 (QuickTime) + if &body[4..8] == b"moov" || &body[4..8] == b"mdat" { return DataType::Video; } + } + + if body.len() >= 4 { // WebM 시그니처 if &body[0..4] == b"\x1a\x45\xdf\xa3" { return DataType::Video; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd3dffdd..20c4a22c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ importers: '@tauri-apps/api': specifier: ^2 version: 2.8.0 + '@tauri-apps/plugin-clipboard-manager': + specifier: ^2.3.0 + version: 2.3.0 '@tauri-apps/plugin-fs': specifier: ^2.4.1 version: 2.4.2 @@ -177,6 +180,9 @@ importers: react-refresh: specifier: ^0.17.0 version: 0.17.0 + react-scan: + specifier: ^0.4.3 + version: 0.4.3(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react-router-dom@7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@24.7.2)(typescript@5.9.3) @@ -742,6 +748,12 @@ packages: '@bufbuild/protobuf@2.9.0': resolution: {integrity: sha512-rnJenoStJ8nvmt9Gzye8nkYd6V22xUAnu4086ER7h1zJ508vStko4pMvDeQ446ilDTFpV5wnoc5YS7XvMwwMqA==} + '@clack/core@0.3.5': + resolution: {integrity: sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==} + + '@clack/prompts@0.8.2': + resolution: {integrity: sha512-6b9Ab2UiZwJYA9iMyboYyW9yJvAO9V753ZhS+DHKEjZRKAxPPOb7MXXu84lsPFG+vZt6FRFniZ8rXi+zCIw4yQ==} + '@cspotcode/source-map-support@0.8.1': resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} engines: {node: '>=12'} @@ -759,6 +771,162 @@ packages: '@emnapi/wasi-threads@1.1.0': resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@esbuild/aix-ppc64@0.25.11': + resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.11': + resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.11': + resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.11': + resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.11': + resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.11': + resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.11': + resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.11': + resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.11': + resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.11': + resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.11': + resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.11': + resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.11': + resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.11': + resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.11': + resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.11': + resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.11': + resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.11': + resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.11': + resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.11': + resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.11': + resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.11': + resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.11': + resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.11': + resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.11': + resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.11': + resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@floating-ui/core@1.7.3': resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} @@ -1118,9 +1286,23 @@ packages: resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} engines: {node: '>= 10.0.0'} + '@pivanov/utils@0.0.2': + resolution: {integrity: sha512-q9CN0bFWxWgMY5hVVYyBgez1jGiLBa6I+LkG37ycylPhFvEGOOeaADGtUSu46CaZasPnlY8fCdVJZmrgKb1EPA==} + peerDependencies: + react: '>=18' + react-dom: '>=18' + '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@preact/signals-core@1.12.1': + resolution: {integrity: sha512-BwbTXpj+9QutoZLQvbttRg5x3l5468qaV2kufh+51yha1c53ep5dY4kTuZR35+3pAZxpfQerGJiQqg34ZNZ6uA==} + + '@preact/signals@1.3.2': + resolution: {integrity: sha512-naxcJgUJ6BTOROJ7C3QML7KvwKwCXQJYTc5L/b0eEsdYgPB6SxwoQ1vDGcS0Q7GVjAenVq/tXrybVdFShHYZWg==} + peerDependencies: + preact: 10.x + '@prettier/plugin-oxc@0.0.4': resolution: {integrity: sha512-UGXe+g/rSRbglL0FOJiar+a+nUrst7KaFmsg05wYbKiInGWP6eAj/f8A2Uobgo5KxEtb2X10zeflNH6RK2xeIQ==} engines: {node: '>=14'} @@ -1511,6 +1693,15 @@ packages: resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} engines: {node: '>=14.0.0'} + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + '@rsbuild/core@1.3.22': resolution: {integrity: sha512-FGB7m8Tn/uiOhvqk0lw+NRMyD+VYJ+eBqVfpn0X11spkJDiPWn8UkMRvfzCX4XFcNZwRKYuuKJaZK1DNU8UG+w==} engines: {node: '>=16.10.0'} @@ -1970,6 +2161,9 @@ packages: engines: {node: '>= 10'} hasBin: true + '@tauri-apps/plugin-clipboard-manager@2.3.0': + resolution: {integrity: sha512-81NOBA2P+OTY8RLkBwyl9ZR/0CeggLub4F6zxcxUIfFOAqtky7J61+K/MkH2SC1FMxNBxrX0swDuKvkjkHadlA==} + '@tauri-apps/plugin-fs@2.4.2': resolution: {integrity: sha512-YGhmYuTgXGsi6AjoV+5mh2NvicgWBfVJHHheuck6oHD+HC9bVWPaHvCP0/Aw4pHDejwrvT8hE3+zZAaWf+hrig==} @@ -2063,6 +2257,9 @@ packages: '@types/node-forge@1.3.14': resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} + '@types/node@20.19.22': + resolution: {integrity: sha512-hRnu+5qggKDSyWHlnmThnUqg62l29Aj/6vcYgUaSFL9oc7DVjeWEQN3PRgdSc6F8d9QRMWkf36CLMch1Do/+RQ==} + '@types/node@24.7.2': resolution: {integrity: sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==} @@ -2077,6 +2274,11 @@ packages: peerDependencies: '@types/react': ^19.2.0 + '@types/react-reconciler@0.28.9': + resolution: {integrity: sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==} + peerDependencies: + '@types/react': '*' + '@types/react@19.2.2': resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} @@ -2280,6 +2482,11 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bippy@0.3.31: + resolution: {integrity: sha512-cCYVokhbNiU3jneh73xBfxm192ZUmJp+sx4A0n7rFxmwwBiUYNavaHorW3A2w0174IPaGBjJJtQNiOBOetoY7A==} + peerDependencies: + react: '>=17.0.1' + body-parser@1.20.3: resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==} engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} @@ -2665,6 +2872,11 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + esbuild@0.25.11: + resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} + engines: {node: '>=18'} + hasBin: true + escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} @@ -2716,6 +2928,9 @@ packages: estree-util-visit@1.2.1: resolution: {integrity: sha512-xbgqcrkIVbIG+lI/gzbvd9SGTJL4zqJKBFttUl5pP27KhAjtMKbX/mQXJ7qgyXpMgVy/zvpm0xoQQaGL8OloOw==} + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} @@ -2811,6 +3026,11 @@ packages: resolution: {integrity: sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==} engines: {node: '>=14.14'} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2839,6 +3059,9 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-tsconfig@4.12.0: + resolution: {integrity: sha512-LScr2aNr2FbjAjZh2C6X6BxRx1/x+aTDExct/xyq2XKbYOiG5c0aK7pMsSuyc0brz3ibr/lbQiHD9jzt4lccJw==} + github-slugger@2.0.0: resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} @@ -3659,6 +3882,16 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + playwright-core@1.56.1: + resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.56.1: + resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==} + engines: {node: '>=18'} + hasBin: true + postcss-loader@8.2.0: resolution: {integrity: sha512-tHX+RkpsXVcc7st4dSdDGliI+r4aAQDuv+v3vFYHixb6YgjreG5AG4SEB0kDK8u2s6htqEEpKlkhSBUTvWKYnA==} engines: {node: '>= 18.12.0'} @@ -3676,6 +3909,9 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + preact@10.27.2: + resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==} + prettier@3.6.2: resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} engines: {node: '>=14'} @@ -3808,6 +4044,26 @@ packages: react-dom: optional: true + react-scan@0.4.3: + resolution: {integrity: sha512-jhAQuQ1nja6HUYrSpbmNFHqZPsRCXk8Yqu0lHoRIw9eb8N96uTfXCpVyQhTTnJ/nWqnwuvxbpKVG/oWZT8+iTQ==} + hasBin: true + peerDependencies: + '@remix-run/react': '>=1.0.0' + next: '>=13.0.0' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-router: ^5.0.0 || ^6.0.0 || ^7.0.0 + react-router-dom: ^5.0.0 || ^6.0.0 || ^7.0.0 + peerDependenciesMeta: + '@remix-run/react': + optional: true + next: + optional: true + react-router: + optional: true + react-router-dom: + optional: true + react-style-singleton@2.2.3: resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} engines: {node: '>=10'} @@ -3906,6 +4162,9 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.10: resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} engines: {node: '>= 0.4'} @@ -4140,6 +4399,9 @@ packages: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} + sisteransi@1.0.5: + resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + sockjs@0.3.24: resolution: {integrity: sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==} @@ -4326,6 +4588,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.20.6: + resolution: {integrity: sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==} + engines: {node: '>=18.0.0'} + hasBin: true + tw-animate-css@1.4.0: resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} @@ -4338,6 +4605,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.14.0: resolution: {integrity: sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==} @@ -4407,6 +4677,10 @@ packages: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} + unplugin@2.1.0: + resolution: {integrity: sha512-us4j03/499KhbGP8BU7Hrzrgseo+KdfJYWcbcajCOqsAyb8Gk0Yn2kiUIcZISYCb1JFaZfIuG3b42HmguVOKCQ==} + engines: {node: '>=18.12.0'} + update-browserslist-db@1.1.3: resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} hasBin: true @@ -4520,6 +4794,9 @@ packages: resolution: {integrity: sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==} engines: {node: '>=10.13.0'} + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + webpack@5.102.1: resolution: {integrity: sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==} engines: {node: '>=10.13.0'} @@ -5358,6 +5635,17 @@ snapshots: '@bufbuild/protobuf@2.9.0': {} + '@clack/core@0.3.5': + dependencies: + picocolors: 1.1.1 + sisteransi: 1.0.5 + + '@clack/prompts@0.8.2': + dependencies: + '@clack/core': 0.3.5 + picocolors: 1.1.1 + sisteransi: 1.0.5 + '@cspotcode/source-map-support@0.8.1': dependencies: '@jridgewell/trace-mapping': 0.3.9 @@ -5380,6 +5668,84 @@ snapshots: tslib: 2.8.1 optional: true + '@esbuild/aix-ppc64@0.25.11': + optional: true + + '@esbuild/android-arm64@0.25.11': + optional: true + + '@esbuild/android-arm@0.25.11': + optional: true + + '@esbuild/android-x64@0.25.11': + optional: true + + '@esbuild/darwin-arm64@0.25.11': + optional: true + + '@esbuild/darwin-x64@0.25.11': + optional: true + + '@esbuild/freebsd-arm64@0.25.11': + optional: true + + '@esbuild/freebsd-x64@0.25.11': + optional: true + + '@esbuild/linux-arm64@0.25.11': + optional: true + + '@esbuild/linux-arm@0.25.11': + optional: true + + '@esbuild/linux-ia32@0.25.11': + optional: true + + '@esbuild/linux-loong64@0.25.11': + optional: true + + '@esbuild/linux-mips64el@0.25.11': + optional: true + + '@esbuild/linux-ppc64@0.25.11': + optional: true + + '@esbuild/linux-riscv64@0.25.11': + optional: true + + '@esbuild/linux-s390x@0.25.11': + optional: true + + '@esbuild/linux-x64@0.25.11': + optional: true + + '@esbuild/netbsd-arm64@0.25.11': + optional: true + + '@esbuild/netbsd-x64@0.25.11': + optional: true + + '@esbuild/openbsd-arm64@0.25.11': + optional: true + + '@esbuild/openbsd-x64@0.25.11': + optional: true + + '@esbuild/openharmony-arm64@0.25.11': + optional: true + + '@esbuild/sunos-x64@0.25.11': + optional: true + + '@esbuild/win32-arm64@0.25.11': + optional: true + + '@esbuild/win32-ia32@0.25.11': + optional: true + + '@esbuild/win32-x64@0.25.11': + optional: true + '@floating-ui/core@1.7.3': dependencies: '@floating-ui/utils': 0.2.10 @@ -5712,8 +6078,20 @@ snapshots: '@parcel/watcher-win32-x64': 2.5.1 optional: true + '@pivanov/utils@0.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0)': + dependencies: + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + '@polka/url@1.0.0-next.29': {} + '@preact/signals-core@1.12.1': {} + + '@preact/signals@1.3.2(preact@10.27.2)': + dependencies: + '@preact/signals-core': 1.12.1 + preact: 10.27.2 + '@prettier/plugin-oxc@0.0.4': dependencies: oxc-parser: 0.74.0 @@ -6112,6 +6490,12 @@ snapshots: '@remix-run/router@1.23.0': {} + '@rollup/pluginutils@5.3.0': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + '@rsbuild/core@1.3.22': dependencies: '@rspack/core': 1.3.12(@swc/helpers@0.5.17) @@ -6587,6 +6971,10 @@ snapshots: '@tauri-apps/cli-win32-ia32-msvc': 2.8.4 '@tauri-apps/cli-win32-x64-msvc': 2.8.4 + '@tauri-apps/plugin-clipboard-manager@2.3.0': + dependencies: + '@tauri-apps/api': 2.8.0 + '@tauri-apps/plugin-fs@2.4.2': dependencies: '@tauri-apps/api': 2.8.0 @@ -6702,6 +7090,10 @@ snapshots: dependencies: '@types/node': 24.7.2 + '@types/node@20.19.22': + dependencies: + undici-types: 6.21.0 + '@types/node@24.7.2': dependencies: undici-types: 7.14.0 @@ -6714,6 +7106,10 @@ snapshots: dependencies: '@types/react': 19.2.2 + '@types/react-reconciler@0.28.9(@types/react@19.2.2)': + dependencies: + '@types/react': 19.2.2 + '@types/react@19.2.2': dependencies: csstype: 3.1.3 @@ -6941,6 +7337,13 @@ snapshots: binary-extensions@2.3.0: {} + bippy@0.3.31(@types/react@19.2.2)(react@19.2.0): + dependencies: + '@types/react-reconciler': 0.28.9(@types/react@19.2.2) + react: 19.2.0 + transitivePeerDependencies: + - '@types/react' + body-parser@1.20.3: dependencies: bytes: 3.1.2 @@ -7289,6 +7692,35 @@ snapshots: dependencies: es-errors: 1.3.0 + esbuild@0.25.11: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.11 + '@esbuild/android-arm': 0.25.11 + '@esbuild/android-arm64': 0.25.11 + '@esbuild/android-x64': 0.25.11 + '@esbuild/darwin-arm64': 0.25.11 + '@esbuild/darwin-x64': 0.25.11 + '@esbuild/freebsd-arm64': 0.25.11 + '@esbuild/freebsd-x64': 0.25.11 + '@esbuild/linux-arm': 0.25.11 + '@esbuild/linux-arm64': 0.25.11 + '@esbuild/linux-ia32': 0.25.11 + '@esbuild/linux-loong64': 0.25.11 + '@esbuild/linux-mips64el': 0.25.11 + '@esbuild/linux-ppc64': 0.25.11 + '@esbuild/linux-riscv64': 0.25.11 + '@esbuild/linux-s390x': 0.25.11 + '@esbuild/linux-x64': 0.25.11 + '@esbuild/netbsd-arm64': 0.25.11 + '@esbuild/netbsd-x64': 0.25.11 + '@esbuild/openbsd-arm64': 0.25.11 + '@esbuild/openbsd-x64': 0.25.11 + '@esbuild/openharmony-arm64': 0.25.11 + '@esbuild/sunos-x64': 0.25.11 + '@esbuild/win32-arm64': 0.25.11 + '@esbuild/win32-ia32': 0.25.11 + '@esbuild/win32-x64': 0.25.11 + escalade@3.2.0: {} escape-html@1.0.3: {} @@ -7335,6 +7767,8 @@ snapshots: '@types/estree-jsx': 1.0.5 '@types/unist': 2.0.11 + estree-walker@2.0.2: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 @@ -7444,6 +7878,9 @@ snapshots: jsonfile: 6.2.0 universalify: 2.0.1 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -7473,6 +7910,10 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-tsconfig@4.12.0: + dependencies: + resolve-pkg-maps: 1.0.0 + github-slugger@2.0.0: {} glob-parent@5.1.2: @@ -8534,6 +8975,14 @@ snapshots: pirates@4.0.7: {} + playwright-core@1.56.1: {} + + playwright@1.56.1: + dependencies: + playwright-core: 1.56.1 + optionalDependencies: + fsevents: 2.3.2 + postcss-loader@8.2.0(@rspack/core@1.5.8(@swc/helpers@0.5.17))(postcss@8.5.6)(typescript@5.9.3)(webpack@5.102.1): dependencies: cosmiconfig: 9.0.0(typescript@5.9.3) @@ -8552,6 +9001,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + preact@10.27.2: {} + prettier@3.6.2: {} prismjs@1.27.0: {} @@ -8675,6 +9126,36 @@ snapshots: optionalDependencies: react-dom: 19.2.0(react@19.2.0) + react-scan@0.4.3(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react-router-dom@7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react-router@7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0))(react@19.2.0): + dependencies: + '@babel/core': 7.28.4 + '@babel/generator': 7.28.3 + '@babel/types': 7.28.4 + '@clack/core': 0.3.5 + '@clack/prompts': 0.8.2 + '@pivanov/utils': 0.0.2(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@preact/signals': 1.3.2(preact@10.27.2) + '@rollup/pluginutils': 5.3.0 + '@types/node': 20.19.22 + bippy: 0.3.31(@types/react@19.2.2)(react@19.2.0) + esbuild: 0.25.11 + estree-walker: 3.0.3 + kleur: 4.1.5 + mri: 1.2.0 + playwright: 1.56.1 + preact: 10.27.2 + react: 19.2.0 + react-dom: 19.2.0(react@19.2.0) + tsx: 4.20.6 + optionalDependencies: + react-router: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + react-router-dom: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + unplugin: 2.1.0 + transitivePeerDependencies: + - '@types/react' + - rollup + - supports-color + react-style-singleton@2.2.3(@types/react@19.2.2)(react@19.2.0): dependencies: get-nonce: 1.0.1 @@ -8814,6 +9295,8 @@ snapshots: resolve-from@4.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve@1.22.10: dependencies: is-core-module: 2.16.1 @@ -9072,6 +9555,8 @@ snapshots: mrmime: 2.0.1 totalist: 3.0.1 + sisteransi@1.0.5: {} + sockjs@0.3.24: dependencies: faye-websocket: 0.11.4 @@ -9249,6 +9734,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.20.6: + dependencies: + esbuild: 0.25.11 + get-tsconfig: 4.12.0 + optionalDependencies: + fsevents: 2.3.3 + tw-animate-css@1.4.0: {} type-is@1.6.18: @@ -9258,6 +9750,8 @@ snapshots: typescript@5.9.3: {} + undici-types@6.21.0: {} + undici-types@7.14.0: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -9342,6 +9836,12 @@ snapshots: unpipe@1.0.0: {} + unplugin@2.1.0: + dependencies: + acorn: 8.15.0 + webpack-virtual-modules: 0.6.2 + optional: true + update-browserslist-db@1.1.3(browserslist@4.26.3): dependencies: browserslist: 4.26.3 @@ -9493,6 +9993,9 @@ snapshots: webpack-sources@3.3.3: {} + webpack-virtual-modules@0.6.2: + optional: true + webpack@5.102.1: dependencies: '@types/eslint-scope': 3.7.7 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8ef8717a..598318bd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,6 @@ packages: - - "tauri-ui" - - "docs" + - tauri-ui + - docs + +onlyBuiltDependencies: + - esbuild diff --git a/tauri-ui/package.json b/tauri-ui/package.json index 36318d00..bd865cba 100644 --- a/tauri-ui/package.json +++ b/tauri-ui/package.json @@ -30,6 +30,7 @@ "@tanstack/react-form": "^1.19.5", "@tanstack/react-virtual": "^3.13.12", "@tauri-apps/api": "^2", + "@tauri-apps/plugin-clipboard-manager": "^2.3.0", "@tauri-apps/plugin-fs": "^2.4.1", "@tauri-apps/plugin-http": "^2.5.1", "@tauri-apps/plugin-opener": "^2.4.0", @@ -68,6 +69,7 @@ "babel-plugin-react-compiler": "^1.0.0", "react-refresh": "^0.17.0", "ts-node": "^10.9.2", + "react-scan": "^0.4.3", "typescript": "^5.8.3" } } diff --git a/tauri-ui/rspack.config.ts b/tauri-ui/rspack.config.ts index f43e2f59..bea5edb5 100644 --- a/tauri-ui/rspack.config.ts +++ b/tauri-ui/rspack.config.ts @@ -104,4 +104,8 @@ export default defineConfig({ css: true, topLevelAwait: true, }, + // Worker 지원을 위한 설정 + output: { + workerChunkLoading: 'import-scripts', + }, }); diff --git a/tauri-ui/src-tauri/Cargo.toml b/tauri-ui/src-tauri/Cargo.toml index 72735de4..268ad142 100644 --- a/tauri-ui/src-tauri/Cargo.toml +++ b/tauri-ui/src-tauri/Cargo.toml @@ -28,6 +28,7 @@ tauri-plugin-opener = "2" tauri-plugin-store = "2" tauri-plugin-http = "2" tauri-plugin-devtools = "2" +tauri-plugin-clipboard-manager = "2" tokio = { version = "1", features = ["full"] } proxyapi = {path = "../../crates/proxyapi"} proxyapi_models = {path = "../../crates/proxyapi_models"} diff --git a/tauri-ui/src-tauri/capabilities/default.json b/tauri-ui/src-tauri/capabilities/default.json index 59f4493a..a807afe8 100644 --- a/tauri-ui/src-tauri/capabilities/default.json +++ b/tauri-ui/src-tauri/capabilities/default.json @@ -2,7 +2,9 @@ "$schema": "../gen/schemas/desktop-schema.json", "identifier": "default", "description": "Capability for the main window", - "windows": ["main"], + "windows": [ + "main" + ], "permissions": [ "core:default", "opener:default", @@ -11,7 +13,15 @@ "fs:default", { "identifier": "fs:allow-read", - "allow": ["$APPDATA/**", "$APPCONFIG/**", "$APPCONFIGDATA/**"] - } + "allow": [ + "$APPDATA/**", + "$APPCONFIG/**", + "$APPCONFIGDATA/**" + ] + }, + "clipboard-manager:allow-write-text", + "clipboard-manager:allow-write-image", + "clipboard-manager:allow-read-text", + "clipboard-manager:allow-read-image" ] } diff --git a/tauri-ui/src-tauri/src/lib.rs b/tauri-ui/src-tauri/src/lib.rs index 6102a2aa..564f75db 100644 --- a/tauri-ui/src-tauri/src/lib.rs +++ b/tauri-ui/src-tauri/src/lib.rs @@ -20,6 +20,7 @@ pub fn run() { .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_fs::init()) + .plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_store::Builder::default().build()); // DevTools 플러그인 추가 (개발 빌드에서만) diff --git a/tauri-ui/src/features/transaction-details/lib/utils.ts b/tauri-ui/src/features/transaction-details/lib/utils.ts index 17e2b7a8..2e4075aa 100644 --- a/tauri-ui/src/features/transaction-details/lib/utils.ts +++ b/tauri-ui/src/features/transaction-details/lib/utils.ts @@ -32,6 +32,109 @@ export const decodeHtmlEntities = (text: string): string => { return textarea.value; }; +/** + * Uint8Array를 Base64 문자열로 변환 + */ +export const uint8ArrayToBase64 = (data: Uint8Array | number[]): string => { + if (!data || data.length === 0) { + return ''; + } + + try { + // 일반 배열인 경우 Uint8Array로 변환 + const uint8Array = data instanceof Uint8Array ? data : new Uint8Array(data); + + // Uint8Array를 문자열로 변환한 후 Base64 인코딩 + let binary = ''; + for (let i = 0; i < uint8Array.length; i++) { + binary += String.fromCharCode(uint8Array[i]); + } + return btoa(binary); + } catch (error) { + console.error('Base64 인코딩 실패:', error); + return ''; + } +}; + +/** + * 이미지 데이터를 Data URL로 변환 + */ +export const createImageDataUrl = (data: Uint8Array | number[], dataType: DataType): string => { + if (dataType !== 'Image') { + return ''; + } + + const base64 = uint8ArrayToBase64(data); + if (!base64) { + return ''; + } + + // MIME 타입 결정 + let mimeType = 'image/png'; // 기본값 + if (data.length >= 2) { + const uint8Array = data instanceof Uint8Array ? data : new Uint8Array(data); + + // PNG 시그니처 + if ( + uint8Array.length >= 8 && + uint8Array[0] === 0x89 && + uint8Array[1] === 0x50 && + uint8Array[2] === 0x4e && + uint8Array[3] === 0x47 + ) { + mimeType = 'image/png'; + } + // JPEG 시그니처 + else if (uint8Array[0] === 0xff && uint8Array[1] === 0xd8) { + mimeType = 'image/jpeg'; + } + // GIF 시그니처 + else if ( + uint8Array.length >= 6 && + ((uint8Array[0] === 0x47 && + uint8Array[1] === 0x49 && + uint8Array[2] === 0x46 && + uint8Array[3] === 0x38 && + uint8Array[4] === 0x37 && + uint8Array[5] === 0x61) || + (uint8Array[0] === 0x47 && + uint8Array[1] === 0x49 && + uint8Array[2] === 0x46 && + uint8Array[3] === 0x38 && + uint8Array[4] === 0x39 && + uint8Array[5] === 0x61)) + ) { + mimeType = 'image/gif'; + } + // WebP 시그니처 + else if ( + uint8Array.length >= 12 && + uint8Array[0] === 0x52 && + uint8Array[1] === 0x49 && + uint8Array[2] === 0x46 && + uint8Array[3] === 0x46 && + uint8Array[8] === 0x57 && + uint8Array[9] === 0x45 && + uint8Array[10] === 0x42 && + uint8Array[11] === 0x50 + ) { + mimeType = 'image/webp'; + } + // SVG는 텍스트 기반이므로 별도 처리 + else if ( + uint8Array.length >= 4 && + uint8Array[0] === 0x3c && + uint8Array[1] === 0x73 && + uint8Array[2] === 0x76 && + uint8Array[3] === 0x67 + ) { + mimeType = 'image/svg+xml'; + } + } + + return `data:${mimeType};base64,${base64}`; +}; + /** * 요청/응답 본문을 포맷팅된 문자열로 변환 * 러스트에서 이미 데이터 타입 감지와 압축 해제를 완료했으므로 단순한 포맷팅만 수행 diff --git a/tauri-ui/src/features/transaction-details/ui/image-preview.tsx b/tauri-ui/src/features/transaction-details/ui/image-preview.tsx new file mode 100644 index 00000000..7d888345 --- /dev/null +++ b/tauri-ui/src/features/transaction-details/ui/image-preview.tsx @@ -0,0 +1,174 @@ +import { useState, useEffect } from 'react'; +import { Download, Maximize2, Minimize2 } from 'lucide-react'; + +import { Button } from '@/shared/ui'; +import { createImageDataUrl } from '../lib/utils'; +import { useBase64Worker } from '@/hooks/use-base64-worker'; +import type { DataType } from '@/entities/proxy/model/types'; + +interface ImagePreviewProps { + data: Uint8Array | number[]; + dataType: DataType; + className?: string; +} + +export const ImagePreview = ({ data, dataType, className = '' }: ImagePreviewProps) => { + const [isFullscreen, setIsFullscreen] = useState(false); + const [imageError, setImageError] = useState(false); + const [dataUrl, setDataUrl] = useState(''); + const [isLoading, setIsLoading] = useState(true); + + const { encodeToBase64, isWorkerAvailable } = useBase64Worker(); + + useEffect(() => { + if (dataType === 'Image') { + setIsLoading(true); + setImageError(false); + + // Worker가 사용 가능한 경우 Worker 사용, 그렇지 않으면 기존 방식 사용 + if (isWorkerAvailable()) { + encodeToBase64(data, dataType) + .then(setDataUrl) + .catch(() => { + // Worker 실패 시 기존 방식으로 fallback + const fallbackDataUrl = createImageDataUrl(data, dataType); + if (fallbackDataUrl) { + setDataUrl(fallbackDataUrl); + } else { + setImageError(true); + } + }) + .finally(() => { + setIsLoading(false); + }); + } else { + // Worker를 사용할 수 없는 경우 기존 방식 사용 + const fallbackDataUrl = createImageDataUrl(data, dataType); + if (fallbackDataUrl) { + setDataUrl(fallbackDataUrl); + } else { + setImageError(true); + } + setIsLoading(false); + } + } else { + setIsLoading(false); + } + }, [data, dataType, encodeToBase64, isWorkerAvailable]); + + if (isLoading) { + return ( +
+
+
+

이미지 로딩 중...

+

+ {data.length > 1024 * 1024 + ? `${Math.round(data.length / 1024 / 1024)}MB` + : `${Math.round(data.length / 1024)}KB`} +

+
+
+ ); + } + + if (!dataUrl || imageError) { + return ( +
+
+

이미지를 표시할 수 없습니다

+

+ {dataType} 파일 ({data.length} bytes) +

+
+
+ ); + } + + const handleDownload = () => { + const link = document.createElement('a'); + link.href = dataUrl; + link.download = `image.${getFileExtension(dataType)}`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + }; + + const getFileExtension = (type: DataType): string => { + switch (type) { + case 'Image': + // MIME 타입에서 확장자 추출 + const mimeType = dataUrl.split(';')[0].split(':')[1]; + switch (mimeType) { + case 'image/png': + return 'png'; + case 'image/jpeg': + return 'jpg'; + case 'image/gif': + return 'gif'; + case 'image/webp': + return 'webp'; + case 'image/svg+xml': + return 'svg'; + default: + return 'png'; + } + default: + return 'bin'; + } + }; + + const imageElement = ( +
+ 이미지 미리보기 setImageError(true)} + style={{ + maxWidth: isFullscreen ? '90vw' : '100%', + maxHeight: isFullscreen ? '90vh' : '400px', + }} + /> + + {/* 호버 시 컨트롤 버튼들 */} +
+
+ + +
+
+
+ ); + + if (isFullscreen) { + return ( +
+
+ {imageElement} + +
+
+ ); + } + + return ( +
+ {imageElement} +
+ {dataType} 파일 ({data.length} bytes) +
+
+ ); +}; diff --git a/tauri-ui/src/features/transaction-details/ui/transaction-body.tsx b/tauri-ui/src/features/transaction-details/ui/transaction-body.tsx index 26931fad..42ac9273 100644 --- a/tauri-ui/src/features/transaction-details/ui/transaction-body.tsx +++ b/tauri-ui/src/features/transaction-details/ui/transaction-body.tsx @@ -1,4 +1,5 @@ import { Copy } from 'lucide-react'; +import { writeImage, writeText } from '@tauri-apps/plugin-clipboard-manager'; import type { HttpTransaction } from '@/entities/proxy'; @@ -6,9 +7,10 @@ import { Button, Card, CardContent, CardHeader } from '@/shared/ui'; import type { AppFormInstance } from '../context/form-context'; import { Editor } from '@monaco-editor/react'; -import { getBodyForDisplay } from '../lib/utils'; -import { dataTypeToMonacoLanguage } from '@/entities/proxy/model/data-type'; +import { getBodyForDisplay, createImageDataUrl } from '../lib/utils'; +import { dataTypeToMonacoLanguage, isImageDataType } from '@/entities/proxy/model/data-type'; import { toast } from 'sonner'; +import { ImagePreview } from './image-preview'; interface TransactionBodyProps { transaction: HttpTransaction; @@ -30,9 +32,60 @@ export const TransactionBody = ({ transaction, isEditing = false, form }: Transa const requestText = getRequestText(); - const handleCopy = () => { - navigator.clipboard.writeText(requestText); - toast.success('Request body copied to clipboard'); + const handleCopy = async () => { + if (request?.body && request.body.length > 0 && isImageDataType(request.data_type)) { + try { + console.log('Attempting to copy image:', { + dataType: request.data_type, + dataLength: request.body.length, + dataFirst10Bytes: Array.from(request.body.slice(0, 10)), + }); + + // Tauri 클립보드 매니저를 사용하여 이미지 복사 + await writeImage(request.body); + console.log('Image copied successfully via Tauri clipboard manager'); + toast.success('Image copied to clipboard'); + } catch (error) { + console.error('Failed to copy image via Tauri clipboard manager:', error); + toast.error('Failed to copy image'); + + // Tauri 클립보드 실패 시 다운로드로 fallback + try { + console.log('Attempting fallback download...'); + const dataUrl = createImageDataUrl(request.body, request.data_type); + if (dataUrl) { + const link = document.createElement('a'); + link.href = dataUrl; + link.download = `image.${getImageFileExtension(request.data_type)}`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + toast.success('Image downloaded (clipboard failed)'); + console.log('Image downloaded as fallback'); + } else { + console.error('Failed to generate dataUrl for fallback download'); + toast.error('Failed to copy or download image'); + } + } catch (fallbackError) { + console.error('Fallback download failed:', fallbackError); + toast.error('Failed to copy or download image'); + } + } + } else { + try { + // Tauri 클립보드 매니저를 사용하여 텍스트 복사 + await writeText(requestText); + toast.success('Request body copied to clipboard'); + } catch (error) { + console.error('Failed to copy text:', error); + toast.error('Failed to copy to clipboard'); + } + } + }; + + const getImageFileExtension = (dataType: string): string => { + // MIME 타입에서 확장자 추출하는 간단한 함수 + return 'png'; // 기본값 }; return ( @@ -45,7 +98,11 @@ export const TransactionBody = ({ transaction, isEditing = false, form }: Transa - {form && isEditing ? ( + {request?.body && request.body.length > 0 && isImageDataType(request.data_type) ? ( +
+ +
+ ) : form && isEditing ? ( ( diff --git a/tauri-ui/src/features/transaction-details/ui/transaction-response.tsx b/tauri-ui/src/features/transaction-details/ui/transaction-response.tsx index 8c3e87f1..b09c43c2 100644 --- a/tauri-ui/src/features/transaction-details/ui/transaction-response.tsx +++ b/tauri-ui/src/features/transaction-details/ui/transaction-response.tsx @@ -1,4 +1,5 @@ import { Copy } from 'lucide-react'; +import { writeImage, writeText } from '@tauri-apps/plugin-clipboard-manager'; import type { HttpTransaction } from '@/entities/proxy'; @@ -6,9 +7,10 @@ import { Button, Card, CardContent, CardHeader } from '@/shared/ui'; import type { AppFormInstance } from '../context/form-context'; import { Editor } from '@monaco-editor/react'; -import { getBodyForDisplay } from '../lib/utils'; -import { dataTypeToMonacoLanguage } from '@/entities/proxy/model/data-type'; +import { getBodyForDisplay, createImageDataUrl } from '../lib/utils'; +import { dataTypeToMonacoLanguage, isImageDataType } from '@/entities/proxy/model/data-type'; import { toast } from 'sonner'; +import { ImagePreview } from './image-preview'; interface TransactionResponseProps { transaction: HttpTransaction; @@ -27,9 +29,60 @@ export const TransactionResponse = ({ transaction, isEditing = false, form }: Tr const responseText = getResponseText(); - const handleCopy = () => { - navigator.clipboard.writeText(responseText); - toast.success('Response body copied to clipboard'); + const handleCopy = async () => { + if (isImageDataType(response.data_type)) { + try { + console.log('Attempting to copy image:', { + dataType: response.data_type, + dataLength: response.body.length, + dataFirst10Bytes: Array.from(response.body.slice(0, 10)), + }); + + // Tauri 클립보드 매니저를 사용하여 이미지 복사 + await writeImage(response.body); + console.log('Image copied successfully via Tauri clipboard manager'); + toast.success('Image copied to clipboard'); + } catch (error) { + console.error('Failed to copy image via Tauri clipboard manager:', error); + toast.error('Failed to copy image'); + + // Tauri 클립보드 실패 시 다운로드로 fallback + try { + console.log('Attempting fallback download...'); + const dataUrl = createImageDataUrl(response.body, response.data_type); + if (dataUrl) { + const link = document.createElement('a'); + link.href = dataUrl; + link.download = `image.${getImageFileExtension(response.data_type)}`; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + toast.success('Image downloaded (clipboard failed)'); + console.log('Image downloaded as fallback'); + } else { + console.error('Failed to generate dataUrl for fallback download'); + toast.error('Failed to copy or download image'); + } + } catch (fallbackError) { + console.error('Fallback download failed:', fallbackError); + toast.error('Failed to copy or download image'); + } + } + } else { + try { + // Tauri 클립보드 매니저를 사용하여 텍스트 복사 + await writeText(responseText); + toast.success('Response body copied to clipboard'); + } catch (error) { + console.error('Failed to copy text:', error); + toast.error('Failed to copy to clipboard'); + } + } + }; + + const getImageFileExtension = (dataType: string): string => { + // MIME 타입에서 확장자 추출하는 간단한 함수 + return 'png'; // 기본값 }; return ( @@ -42,7 +95,11 @@ export const TransactionResponse = ({ transaction, isEditing = false, form }: Tr - {form && isEditing ? ( + {isImageDataType(response.data_type) ? ( +
+ +
+ ) : form && isEditing ? ( ( diff --git a/tauri-ui/src/hooks/use-base64-worker.ts b/tauri-ui/src/hooks/use-base64-worker.ts new file mode 100644 index 00000000..8704439c --- /dev/null +++ b/tauri-ui/src/hooks/use-base64-worker.ts @@ -0,0 +1,115 @@ +import { useCallback, useRef, useEffect } from 'react'; + +interface WorkerTask { + id: string; + resolve: (value: string) => void; + reject: (error: Error) => void; +} + +interface WorkerMessage { + id: string; + data: Uint8Array | number[]; + dataType: string; +} + +interface WorkerResponse { + id: string; + success: boolean; + dataUrl?: string; + error?: string; +} + +/** + * Base64 변환을 위한 Web Worker 관리 Hook + * 메인 스레드를 블로킹하지 않고 이미지 데이터를 Base64로 변환 + */ +export const useBase64Worker = () => { + const workerRef = useRef(null); + const tasksRef = useRef>(new Map()); + const taskIdRef = useRef(0); + + useEffect(() => { + // Worker 생성 + try { + workerRef.current = new Worker(new URL('../workers/base64-worker.ts', import.meta.url)); + + workerRef.current.onmessage = (e: MessageEvent) => { + const { id, success, dataUrl, error } = e.data; + const task = tasksRef.current.get(id); + + if (task) { + tasksRef.current.delete(id); + if (success && dataUrl) { + task.resolve(dataUrl); + } else { + task.reject(new Error(error || 'Unknown error')); + } + } + }; + + workerRef.current.onerror = () => { + // 모든 대기 중인 작업을 실패로 처리 + tasksRef.current.forEach((task) => { + task.reject(new Error('Worker error')); + }); + tasksRef.current.clear(); + }; + } catch { + // Worker 생성 실패 시 조용히 처리 + } + + return () => { + if (workerRef.current) { + // 대기 중인 모든 작업을 실패로 처리 + tasksRef.current.forEach((task) => { + task.reject(new Error('Worker terminated')); + }); + tasksRef.current.clear(); + + workerRef.current.terminate(); + workerRef.current = null; + } + }; + }, []); + + /** + * 이미지 데이터를 Base64 Data URL로 변환 + */ + const encodeToBase64 = useCallback((data: Uint8Array | number[], dataType: string): Promise => { + return new Promise((resolve, reject) => { + if (!workerRef.current) { + reject(new Error('Worker not initialized')); + return; + } + + taskIdRef.current += 1; + const id = `task_${taskIdRef.current}`; + tasksRef.current.set(id, { id, resolve, reject }); + + const message: WorkerMessage = { + id, + data, + dataType, + }; + + try { + workerRef.current.postMessage(message); + } catch { + tasksRef.current.delete(id); + reject(new Error('Failed to send message to worker')); + } + }); + }, []); + + /** + * Worker가 사용 가능한지 확인 + */ + const isWorkerAvailable = useCallback(() => { + return workerRef.current !== null; + }, []); + + return { + encodeToBase64, + isWorkerAvailable, + }; +}; diff --git a/tauri-ui/src/main.tsx b/tauri-ui/src/main.tsx index da459f36..a4de752a 100644 --- a/tauri-ui/src/main.tsx +++ b/tauri-ui/src/main.tsx @@ -5,6 +5,13 @@ import './main.css'; import '../styles.css'; import './shared/stores/session-store'; +// react-scan을 개발 환경에서만 실행 +if (process.env.NODE_ENV === 'development') { + import('react-scan').then(({ scan }) => { + scan(); + }); +} + const container = document.getElementById('root'); if (container) { diff --git a/tauri-ui/src/workers/base64-worker.ts b/tauri-ui/src/workers/base64-worker.ts new file mode 100644 index 00000000..f327d1e6 --- /dev/null +++ b/tauri-ui/src/workers/base64-worker.ts @@ -0,0 +1,142 @@ +// Base64 변환 Web Worker +// 메인 스레드를 블로킹하지 않고 이미지 데이터를 Base64로 변환 + +interface WorkerMessage { + id: string; + data: Uint8Array | number[]; + dataType: string; +} + +interface WorkerResponse { + id: string; + success: boolean; + dataUrl?: string; + error?: string; +} + +self.onmessage = function (e: MessageEvent) { + const { data, dataType, id } = e.data; + + try { + if (dataType === 'Image') { + const base64 = uint8ArrayToBase64(data); + const mimeType = getImageMimeType(data); + const dataUrl = `data:${mimeType};base64,${base64}`; + + const response: WorkerResponse = { + id, + success: true, + dataUrl, + }; + + self.postMessage(response); + } else { + const response: WorkerResponse = { + id, + success: false, + error: 'Not an image', + }; + + self.postMessage(response); + } + } catch (error) { + const response: WorkerResponse = { + id, + success: false, + error: error instanceof Error ? error.message : 'Unknown error', + }; + + self.postMessage(response); + } +}; + +/** + * Uint8Array를 Base64 문자열로 변환 (청크 단위 처리) + */ +function uint8ArrayToBase64(data: Uint8Array | number[]): string { + if (!data || data.length === 0) { + return ''; + } + + const uint8Array = data instanceof Uint8Array ? data : new Uint8Array(data); + + // 청크 단위로 처리하여 메모리 효율성 향상 + const chunkSize = 8192; // 8KB 청크 + let result = ''; + + for (let i = 0; i < uint8Array.length; i += chunkSize) { + const chunk = uint8Array.slice(i, i + chunkSize); + const binary = Array.from(chunk, (byte) => String.fromCharCode(byte)).join(''); + result += btoa(binary); + } + + return result; +} + +/** + * 이미지 데이터의 MIME 타입 결정 + */ +function getImageMimeType(data: Uint8Array | number[]): string { + const uint8Array = data instanceof Uint8Array ? data : new Uint8Array(data); + + if (uint8Array.length >= 2) { + // PNG 시그니처 + if ( + uint8Array.length >= 8 && + uint8Array[0] === 0x89 && + uint8Array[1] === 0x50 && + uint8Array[2] === 0x4e && + uint8Array[3] === 0x47 + ) { + return 'image/png'; + } + // JPEG 시그니처 + if (uint8Array[0] === 0xff && uint8Array[1] === 0xd8) { + return 'image/jpeg'; + } + // GIF 시그니처 + if ( + uint8Array.length >= 6 && + ((uint8Array[0] === 0x47 && + uint8Array[1] === 0x49 && + uint8Array[2] === 0x46 && + uint8Array[3] === 0x38 && + uint8Array[4] === 0x37 && + uint8Array[5] === 0x61) || + (uint8Array[0] === 0x47 && + uint8Array[1] === 0x49 && + uint8Array[2] === 0x46 && + uint8Array[3] === 0x38 && + uint8Array[4] === 0x39 && + uint8Array[5] === 0x61)) + ) { + return 'image/gif'; + } + // WebP 시그니처 + if ( + uint8Array.length >= 12 && + uint8Array[0] === 0x52 && + uint8Array[1] === 0x49 && + uint8Array[2] === 0x46 && + uint8Array[3] === 0x46 && + uint8Array[8] === 0x57 && + uint8Array[9] === 0x45 && + uint8Array[10] === 0x42 && + uint8Array[11] === 0x50 + ) { + return 'image/webp'; + } + // SVG 시그니처 + if ( + uint8Array.length >= 4 && + uint8Array[0] === 0x3c && + uint8Array[1] === 0x73 && + uint8Array[2] === 0x76 && + uint8Array[3] === 0x67 + ) { + return 'image/svg+xml'; + } + } + + return 'image/png'; // 기본값 +}