diff --git a/package-lock.json b/package-lock.json index 690d90e..a75144d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "hardware-visualizer", "version": "0.0.1", "dependencies": { - "@hookform/resolvers": "^3.9.0", + "@hookform/resolvers": "^3.9.1", "@phosphor-icons/react": "^2.1.7", "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", @@ -984,9 +984,9 @@ "license": "MIT" }, "node_modules/@hookform/resolvers": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.0.tgz", - "integrity": "sha512-bU0Gr4EepJ/EQsH/IwEzYLsT/PEj5C0ynLQ4m+GSHS+xKH4TfSelhluTgOaoc4kA5s7eCsQbM4wvZLzELmWzUg==", + "version": "3.9.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.9.1.tgz", + "integrity": "sha512-ud2HqmGBM0P0IABqoskKWI6PEf6ZDDBZkFqe2Vnl+mTHCEHzr3ISjjZyCwTjC/qpL25JC9aIDkloQejvMeq0ug==", "license": "MIT", "peerDependencies": { "react-hook-form": "^7.0.0" diff --git a/package.json b/package.json index 584a493..16fad78 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,7 @@ "vitest": "^2.1.3" }, "dependencies": { - "@hookform/resolvers": "^3.9.0", + "@hookform/resolvers": "^3.9.1", "@phosphor-icons/react": "^2.1.7", "@radix-ui/react-checkbox": "^1.1.2", "@radix-ui/react-dialog": "^1.1.2", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 078e527..12084db 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -37,6 +37,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned-vec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4aa90d7ce82d4be67b64039a3d588d38dbcc6736577de4a847025ce5b0c468d1" + [[package]] name = "alloc-no-stdlib" version = "2.0.4" @@ -73,6 +79,23 @@ version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" + +[[package]] +name = "arg_enum_proc_macro" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -254,6 +277,29 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "av1-grain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6678909d8c5d46a42abcf571271e15fdbc0a225e3646cf23762cd415046c78bf" +dependencies = [ + "anyhow", + "arrayvec", + "log", + "nom", + "num-rational", + "v_frame", +] + +[[package]] +name = "avif-serialize" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e335041290c43101ca215eed6f43ec437eb5a42125573f600fc3fa42b9bddd62" +dependencies = [ + "arrayvec", +] + [[package]] name = "backtrace" version = "0.3.74" @@ -281,6 +327,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -296,6 +348,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitstream-io" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b81e1519b0d82120d2fd469d5bfb2919a9361c48b02d82d04befc1cdd2002452" + [[package]] name = "bitvec" version = "1.0.1" @@ -390,6 +448,12 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "built" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c360505aed52b7ec96a3636c3f039d99103c37d1d9b4f7a8c743d3ea9ffcd03b" + [[package]] name = "bumpalo" version = "3.16.0" @@ -430,6 +494,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.7.2" @@ -512,6 +582,8 @@ version = "1.1.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945" dependencies = [ + "jobserver", + "libc", "shlex", ] @@ -605,6 +677,12 @@ dependencies = [ "objc", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "combine" version = "4.6.7" @@ -722,6 +800,12 @@ version = "0.8.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.6" @@ -1049,6 +1133,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.73.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83197f59927b46c04a183a619b7c29df34e63e63c7869320862268c0ef687e0" +dependencies = [ + "bit_field", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "fastrand" version = "2.1.1" @@ -1393,6 +1492,16 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gif" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fb2d69b19215e18bb912fa30f7ce15846e301408695e44e0ef719f1da9e19f2" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.31.1" @@ -1547,11 +1656,23 @@ dependencies = [ "syn 2.0.85", ] +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hardware_visualizer" version = "0.1.0" dependencies = [ + "base64 0.22.1", "chrono", + "image", "nvapi", "rust_decimal", "serde", @@ -1755,6 +1876,45 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "image" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc144d44a31d753b02ce64093d532f55ff8dc4ebf2ffb8a63c0dda691385acae" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "exr", + "gif", + "image-webp", + "num-traits", + "png", + "qoi", + "ravif", + "rayon", + "rgb", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e031e8e3d94711a9ccb5d6ea357439ef3dcbed361798bd4071dc4d9793fbe22f" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "imgref" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0263a3d970d5c054ed9312c0057b4f3bde9c0b33836d3637361d4a9e6e7a408" + [[package]] name = "indexmap" version = "1.9.3" @@ -1795,12 +1955,32 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "interpolate_name" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + [[package]] name = "ipnet" version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "0.4.8" @@ -1858,6 +2038,21 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" + [[package]] name = "js-sys" version = "0.3.72" @@ -1942,6 +2137,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "libappindicator" version = "0.9.0" @@ -1972,6 +2173,17 @@ version = "0.2.159" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" +[[package]] +name = "libfuzzer-sys" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + [[package]] name = "libloading" version = "0.7.4" @@ -2024,6 +2236,15 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "loop9" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fae87c125b03c1d2c0150c90365d7d6bcc53fb73a9acaef207d2d065860f062" +dependencies = [ + "imgref", +] + [[package]] name = "mac" version = "0.1.1" @@ -2068,6 +2289,15 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" +[[package]] +name = "maybe-rayon" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea1f30cedd69f0a2954655f7188c6a834246d2bcf1e315e2ac40c4b24dc9519" +dependencies = [ + "cfg-if", +] + [[package]] name = "memchr" version = "2.7.4" @@ -2089,6 +2319,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.0" @@ -2185,6 +2421,22 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "noop_proc_macro" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0676bb32a98c1a483ce53e500a81ad9c3d5b3f7c920c28c24e9cb0980d0b5bc8" + [[package]] name = "ntapi" version = "0.4.1" @@ -2204,12 +2456,53 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.85", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2584,6 +2877,12 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2874,6 +3173,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "profiling" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d" +dependencies = [ + "profiling-procmacros", +] + +[[package]] +name = "profiling-procmacros" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65f2e60fbf1063868558d69c6beacf412dc755f9fc020f514b7955fc914fe30" +dependencies = [ + "quote", + "syn 2.0.85", +] + [[package]] name = "ptr_meta" version = "0.1.4" @@ -2894,6 +3212,21 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + +[[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.32.0" @@ -3008,6 +3341,55 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "rav1e" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd87ce80a7665b1cce111f8a16c1f3929f6547ce91ade6addf4ec86a8dda5ce9" +dependencies = [ + "arbitrary", + "arg_enum_proc_macro", + "arrayvec", + "av1-grain", + "bitstream-io", + "built", + "cfg-if", + "interpolate_name", + "itertools", + "libc", + "libfuzzer-sys", + "log", + "maybe-rayon", + "new_debug_unreachable", + "noop_proc_macro", + "num-derive", + "num-traits", + "once_cell", + "paste", + "profiling", + "rand 0.8.5", + "rand_chacha 0.3.1", + "simd_helpers", + "system-deps", + "thiserror", + "v_frame", + "wasm-bindgen", +] + +[[package]] +name = "ravif" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2413fd96bd0ea5cdeeb37eaf446a22e6ed7b981d792828721e74ded1980a45c6" +dependencies = [ + "avif-serialize", + "imgref", + "loop9", + "quick-error", + "rav1e", + "rgb", +] + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -3167,6 +3549,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rgb" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57397d16646700483b67d2dd6511d79318f9d057fdbd21a4066aeac8b41d310a" + [[package]] name = "rkyv" version = "0.7.45" @@ -3529,6 +3917,15 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simd_helpers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95890f873bec569a0362c235787f3aca6e1e887302ba4840839bcc6459c42da6" +dependencies = [ + "quote", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -4152,6 +4549,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.36" @@ -4525,6 +4933,17 @@ dependencies = [ "serde", ] +[[package]] +name = "v_frame" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f32aaa24bacd11e488aa9ba66369c7cd514885742c9fe08cfe85884db3e92b" +dependencies = [ + "aligned-vec", + "num-traits", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.0" @@ -4830,6 +5249,12 @@ dependencies = [ "windows-core 0.58.0", ] +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "winapi" version = "0.3.9" @@ -5430,6 +5855,30 @@ dependencies = [ "syn 2.0.85", ] +[[package]] +name = "zune-core" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "zune-jpeg" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16099418600b4d8f028622f73ff6e3deaabdff330fb9a2a131dea781ee8b0768" +dependencies = [ + "zune-core", +] + [[package]] name = "zvariant" version = "4.0.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f5f8100..3f44524 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -40,6 +40,8 @@ wmi = "0.14" rust_decimal = "1.23.0" tauri-plugin-dialog = "2.0.3" tauri-plugin-store = "2.1.0" +base64 = "0.22" +image = "0.25.4" [features] # this feature is used for production builds or when `devPath` points to the filesystem and the built-in dev server is disabled. diff --git a/src-tauri/src/commands/background_image.rs b/src-tauri/src/commands/background_image.rs new file mode 100644 index 0000000..9af7f48 --- /dev/null +++ b/src-tauri/src/commands/background_image.rs @@ -0,0 +1,143 @@ +use base64::encode; +use image::load_from_memory; +use image::ImageFormat; +use serde::{Deserialize, Serialize}; +use std::fs; +use std::fs::File; +use tauri::command; + +use crate::utils::file::get_app_data_dir; + +const FILE_NAME_FORMAT: &str = "bg-img-{}.png"; +const BG_IMG_DIR_NAME: &str = "BgImages"; + +/// +/// 背景画像を取得 +/// +/// - `file_id`: 画像ファイルのインデックス +/// +#[command] +pub fn get_background_image(file_id: String) -> Result { + let dir_path = get_app_data_dir(BG_IMG_DIR_NAME); + let file_name = FILE_NAME_FORMAT.replace("{}", &file_id); + let file_path = dir_path.join(file_name); + + // 画像を読み込んでBase64にエンコード + match fs::read(&file_path) { + Ok(image_data) => Ok(encode(image_data)), + Err(e) => Err(format!("Failed to load image: {}", e)), + } +} + +/// +/// - `file_id` : 画像ファイルID +/// - `image_data` : 画像データのBase64文字列 +/// +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct BackgroundImage { + pub file_id: String, + pub image_data: String, +} + +/// +/// BG_IMG_DIR_NAME ディレクトリ内の背景画像一覧を取得 +/// +#[command] +pub fn get_background_images() -> Result, String> { + let dir_path = get_app_data_dir(BG_IMG_DIR_NAME); + + // ディレクトリ内のファイル一覧を取得 + match fs::read_dir(&dir_path) { + Ok(entries) => { + let images: Vec = entries + .filter_map(|entry| { + let entry = entry.ok()?; + let file_name = entry.file_name().into_string().ok()?; + let file_id = file_name + .strip_prefix("bg-img-") + .and_then(|s| s.strip_suffix(".png"))?; + let file_path = entry.path(); + let image_data = fs::read(&file_path).ok().map(|data| encode(data))?; + Some(BackgroundImage { + file_id: file_id.to_string(), + image_data, + }) + }) + .collect(); + Ok(images) + } + Err(e) => Err(format!("Failed to read directory: {}", e)), + } +} + +/// +/// 背景画像を保存 +/// +/// - `image_data`: 画像データのBase64文字列 +/// - returns: `file_id` +/// +#[command] +pub fn save_background_image(image_data: String) -> Result { + let dir_path = get_app_data_dir(BG_IMG_DIR_NAME); + + // App/BgImages ディレクトリが存在しない場合新規作成 + if !dir_path.parent().unwrap().exists() { + fs::create_dir_all(dir_path.parent().unwrap()).unwrap(); + } + + // Base64データのプレフィックスを除去 + let image_data = if let Some(index) = image_data.find(",") { + &image_data[(index + 1)..] + } else { + &image_data + }; + + // 改行や余分な空白を除去 + let cleaned_data = image_data.replace("\n", "").replace("\r", ""); + + // ディレクトリ内のファイル数を取得し、それをインデックスとして利用 + let file_id: String = match fs::read_dir(&dir_path) { + Ok(entries) => entries.count(), // 現在のファイル数をインデックスとして利用 + Err(_) => 0, // 読み込み失敗の場合は最初のファイルとして 0 + } + .to_string(); + + let file_name = FILE_NAME_FORMAT.replace("{}", &file_id); + let file_path = dir_path.join(file_name); + + // Base64データをデコード + match base64::decode(&cleaned_data) { + Ok(decoded_data) => { + // 画像データをPNGとして保存 + match load_from_memory(&decoded_data) { + Ok(image) => { + let mut file = File::create(&file_path) + .map_err(|e| format!("Failed to create file: {}", e))?; + image + .write_to(&mut file, ImageFormat::Png) + .map_err(|e| format!("Failed to save image as PNG: {}", e))?; + Ok(file_id) + } + Err(e) => Err(format!("Failed to load image from memory: {}", e)), + } + } + Err(e) => Err(format!("Failed to decode image: {}", e)), + } +} + +/// +/// 背景画像を削除 +/// - `file_id`: 画像ファイルのインデックス +/// +#[tauri::command] +pub fn delete_background_image(file_id: String) -> Result<(), String> { + let dir_path = get_app_data_dir(BG_IMG_DIR_NAME); + let file_name = FILE_NAME_FORMAT.replace("{}", &file_id); + let file_path = dir_path.join(file_name); + + match fs::remove_file(&file_path) { + Ok(_) => Ok(()), + Err(e) => Err(format!("Failed to delete image: {}", e)), + } +} diff --git a/src-tauri/src/commands/config.rs b/src-tauri/src/commands/config.rs index 72dd235..ad0b149 100644 --- a/src-tauri/src/commands/config.rs +++ b/src-tauri/src/commands/config.rs @@ -45,6 +45,8 @@ pub struct Settings { line_graph_mix: bool, line_graph_show_legend: bool, line_graph_show_scale: bool, + background_img_opacity: u8, + selected_background_img: Option, state: StateSettings, } @@ -72,6 +74,8 @@ pub struct ClientSettings { line_graph_mix: bool, line_graph_show_legend: bool, line_graph_show_scale: bool, + background_img_opacity: u8, + selected_background_img: Option, state: StateSettings, } @@ -97,6 +101,8 @@ impl Default for Settings { line_graph_mix: false, line_graph_show_legend: true, line_graph_show_scale: false, + background_img_opacity: 50, + selected_background_img: None, state: StateSettings { display: "dashboard".to_string(), }, @@ -297,6 +303,19 @@ impl Settings { self.write_file() } + pub fn set_background_img_opacity(&mut self, new_value: u8) -> Result<(), String> { + self.background_img_opacity = new_value; + self.write_file() + } + + pub fn set_selected_background_img( + &mut self, + new_value: Option, + ) -> Result<(), String> { + self.selected_background_img = new_value; + self.write_file() + } + pub fn set_state(&mut self, key: &str, new_value: String) -> Result<(), String> { match key { "display" => self.state.display = new_value, @@ -390,6 +409,8 @@ pub mod commands { line_graph_mix: settings.line_graph_mix, line_graph_show_legend: settings.line_graph_show_legend, line_graph_show_scale: settings.line_graph_show_scale, + background_img_opacity: settings.background_img_opacity, + selected_background_img: settings.selected_background_img, state: settings.state, }; @@ -551,6 +572,36 @@ pub mod commands { Ok(()) } + #[tauri::command] + pub async fn set_background_img_opacity( + window: Window, + state: tauri::State<'_, AppState>, + new_value: u8, + ) -> Result<(), String> { + let mut settings = state.settings.lock().unwrap(); + + if let Err(e) = settings.set_background_img_opacity(new_value) { + emit_error(&window)?; + return Err(e); + } + Ok(()) + } + + #[tauri::command] + pub async fn set_selected_background_img( + window: Window, + state: tauri::State<'_, AppState>, + file_id: Option, + ) -> Result<(), String> { + let mut settings = state.settings.lock().unwrap(); + + if let Err(e) = settings.set_selected_background_img(file_id) { + emit_error(&window)?; + return Err(e); + } + Ok(()) + } + #[tauri::command] pub async fn set_state( window: Window, diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index 2acb6bb..4af794d 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,2 +1,3 @@ +pub mod background_image; pub mod config; pub mod hardware; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index c233f0c..e182f8d 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,6 +7,7 @@ mod enums; mod services; mod utils; +use commands::background_image; use commands::config; use commands::hardware; use tauri::Manager; @@ -83,7 +84,13 @@ pub fn run() { config::commands::set_line_graph_mix, config::commands::set_line_graph_show_legend, config::commands::set_line_graph_show_scale, + config::commands::set_background_img_opacity, config::commands::set_state, + config::commands::set_selected_background_img, + background_image::get_background_image, + background_image::get_background_images, + background_image::save_background_image, + background_image::delete_background_image, ]) .run(tauri::generate_context!()) .expect("error while running tauri application"); diff --git a/src/App.tsx b/src/App.tsx index 4f3687d..6c127b9 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,6 +8,7 @@ import type { ErrorInfo } from "react"; import { ErrorBoundary } from "react-error-boundary"; import { useSettingsAtom } from "./atom/useSettingsAtom"; import ErrorFallback from "./components/ErrorFallback"; +import { useBackgroundImage } from "./hooks/useBgImage"; import { useDarkMode } from "./hooks/useDarkMode"; import ScreenTemplate from "./template/ScreenTemplate"; import Settings from "./template/Settings"; @@ -25,6 +26,7 @@ const onError = (error: Error, info: ErrorInfo) => { const Page = () => { const { settings } = useSettingsAtom(); const { toggle } = useDarkMode(); + const { backgroundImage } = useBackgroundImage(); useErrorModalListener(); useUsageUpdater("cpu"); @@ -55,9 +57,22 @@ const Page = () => { return ( -
- - {displayTargets[settings.state.display]} +
+ {backgroundImage && ( +
+ )} +
+ + {displayTargets[settings.state.display]} +
); diff --git a/src/atom/useSettingsAtom.ts b/src/atom/useSettingsAtom.ts index f97e2d6..5797545 100644 --- a/src/atom/useSettingsAtom.ts +++ b/src/atom/useSettingsAtom.ts @@ -1,6 +1,7 @@ import { defaultColorRGB } from "@/consts/chart"; import { getSettings, + setBackgroundImgOpacity, setDisplayTargets, setGraphSize, setLanguage, @@ -10,6 +11,7 @@ import { setLineGraphMix, setLineGraphShowLegend, setLineGraphShowScale, + setSelectedBackgroundImg, setState, setTheme, } from "@/services/settingService"; @@ -33,6 +35,8 @@ const settingsAtom = atom({ lineGraphMix: false, lineGraphShowLegend: true, lineGraphShowScale: false, + backgroundImgOpacity: 50, + selectedBackgroundImg: null, state: { display: "dashboard", }, @@ -53,6 +57,8 @@ export const useSettingsAtom = () => { lineGraphMix: setLineGraphMix, lineGraphShowLegend: setLineGraphShowLegend, lineGraphShowScale: setLineGraphShowScale, + backgroundImgOpacity: setBackgroundImgOpacity, + selectedBackgroundImg: setSelectedBackgroundImg, }; const [settings, setSettings] = useAtom(settingsAtom); diff --git a/src/components/forms/SelectBackgroundImage/SelectBackgroundImage.tsx b/src/components/forms/SelectBackgroundImage/SelectBackgroundImage.tsx new file mode 100644 index 0000000..a0a7468 --- /dev/null +++ b/src/components/forms/SelectBackgroundImage/SelectBackgroundImage.tsx @@ -0,0 +1,75 @@ +import { useSettingsAtom } from "@/atom/useSettingsAtom"; +import { Button } from "@/components/ui/button"; +import { useBackgroundImage, useBackgroundImageList } from "@/hooks/useBgImage"; +import { X } from "@phosphor-icons/react"; +import { twMerge } from "tailwind-merge"; +import { tv } from "tailwind-variants"; + +export const BackgroundImageList = () => { + const { settings, updateSettingAtom } = useSettingsAtom(); + const { backgroundImageList } = useBackgroundImageList(); + const { deleteBackgroundImage } = useBackgroundImage(); + + const selectImageVariants = tv({ + base: "relative w-20 h-20 mx-2 rounded-2xl", + variants: { + selected: { + true: "border-2 border-white", + false: "border border-gray-500", + }, + }, + }); + + return ( +
+ {backgroundImageList.length > 0 && ( + + )} + + {backgroundImageList + .slice() + .reverse() + .map((image) => ( +
+ + +
+ ))} +
+ ); +}; diff --git a/src/components/forms/UploadImage/UploadImage.tsx b/src/components/forms/UploadImage/UploadImage.tsx new file mode 100644 index 0000000..9c31296 --- /dev/null +++ b/src/components/forms/UploadImage/UploadImage.tsx @@ -0,0 +1,84 @@ +import { ImageSquare, UploadSimple } from "@phosphor-icons/react"; +import { Button } from "../../ui/button"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "../../ui/form"; +import { Input } from "../../ui/input"; +import { useUploadImage } from "./useUploadImageForm"; + +export const UploadImage = () => { + const { form, picture, onSubmit, fileName, displayUrl } = useUploadImage(); + + return ( +
+ + ( + <> + +
+
+ + Upload Image + + +
+ { + onChange(e.target.files?.[0]); + }} + /> + +
+
+ + The file will only be saved on your device + + +
+
+ +
+
+
+ + )} + /> + + + ); +}; diff --git a/src/components/forms/UploadImage/useUploadImageForm.ts b/src/components/forms/UploadImage/useUploadImageForm.ts new file mode 100644 index 0000000..6102832 --- /dev/null +++ b/src/components/forms/UploadImage/useUploadImageForm.ts @@ -0,0 +1,56 @@ +import { useBackgroundImage } from "@/hooks/useBgImage"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useEffect, useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +const formSchema = z.object({ + picture: z + .instanceof(File, { message: "Please select a file" }) + .refine((file) => ["image/jpeg", "image/png"].includes(file.type), { + message: "Please select JPEG or PNG format images", + }), +}); + +// 画像アップロードカスタムフック +export const useUploadImage = () => { + const [displayUrl, setDisplayUrl] = useState(null); + const [fileName, setFilename] = useState(""); + const { saveBackgroundImage } = useBackgroundImage(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + picture: undefined, + }, + }); + + const onSubmit = async (values: z.infer) => { + try { + await saveBackgroundImage(values.picture); + form.reset(); + } catch (error) { + console.error("Error saveBackgroundImage:", error); + } + }; + + const picture = form.watch("picture"); + + useEffect(() => { + if (picture) { + setFilename(picture.name); + setDisplayUrl(URL.createObjectURL(picture)); + } else { + setFilename(""); + setDisplayUrl(null); + } + }, [picture]); + + return { + form, + picture, + onSubmit, + fileName, + displayUrl, + }; +}; diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..5f806b0 --- /dev/null +++ b/src/components/ui/form.tsx @@ -0,0 +1,183 @@ +import type * as LabelPrimitive from "@radix-ui/react-label"; +import { Slot } from "@radix-ui/react-slot"; +import * as React from "react"; +import { + Controller, + type ControllerProps, + type FieldPath, + type FieldValues, + FormProvider, + useFormContext, +} from "react-hook-form"; + +import { Label } from "@/components/ui/label"; +import { cn } from "@/lib/utils"; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue, +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath, +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error("useFormField should be used within "); + } + + const { id } = itemContext; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +type FormItemContextValue = { + id: string; +}; + +const FormItemContext = React.createContext( + {} as FormItemContextValue, +); + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); +}); +FormItem.displayName = "FormItem"; + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return ( +

Line Color

diff --git a/src/types/settingsType.ts b/src/types/settingsType.ts index 05d3fc3..1b02b90 100644 --- a/src/types/settingsType.ts +++ b/src/types/settingsType.ts @@ -17,7 +17,14 @@ export type Settings = { lineGraphMix: boolean; lineGraphShowLegend: boolean; lineGraphShowScale: boolean; + backgroundImgOpacity: number; + selectedBackgroundImg: string | null; state: { display: SelectedDisplayType; }; }; + +export type BackgroundImage = { + fileId: string; + imageData: string; +};