From 45cebcbd73740e8e6ce14a564c414a977e46f3af Mon Sep 17 00:00:00 2001 From: YuYu1015 Date: Tue, 20 Oct 2026 03:39:59 +0800 Subject: [PATCH 1/2] perf: avoid redoing pointer, scan, and lookup work Pointer movement was re-rendering the page, scans repeated canonicalize and stat calls, and colors and file types were recomputed per item. --- bun.lock | 82 ---- package.json | 4 - src-tauri/Cargo.toml | 4 +- src-tauri/src/main.rs | 286 +++++++------- src/app/analyze/page.tsx | 252 ++++++------- src/app/globals.css | 99 ----- src/app/home/page.tsx | 37 +- src/components/CursorGlow.tsx | 49 +++ src/components/StatsDisplay.tsx | 59 +-- src/hooks/useCountAnimation.ts | 61 --- src/lib/fileTypeUtils.ts | 641 ++++++++------------------------ 11 files changed, 476 insertions(+), 1098 deletions(-) create mode 100644 src/components/CursorGlow.tsx delete mode 100644 src/hooks/useCountAnimation.ts diff --git a/bun.lock b/bun.lock index 8288395..256b1bb 100644 --- a/bun.lock +++ b/bun.lock @@ -13,17 +13,13 @@ "@tauri-apps/plugin-shell": "^2.3.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "d3-shape": "^3.2.0", "i18next": "^25.6.0", - "i18next-resources-to-backend": "^1.2.1", "lucide-react": "^0.545.0", "next": "15.5.7", "next-themes": "^0.4.6", "react": "^19.2.0", "react-dom": "^19.2.0", "react-i18next": "^16.0.1", - "recharts": "^3.2.1", - "recharts-scale": "^0.4.5", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1", }, @@ -156,12 +152,6 @@ "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], - "@reduxjs/toolkit": ["@reduxjs/toolkit@2.9.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^10.0.3", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-fSfQlSRu9Z5yBkvsNhYF2rPS8cGXn/TZVrlwN1948QyZ8xMZ0JvP50S2acZNaf+o63u6aEeMjipFyksjIcWrog=="], - - "@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="], - - "@standard-schema/utils": ["@standard-schema/utils@0.3.0", "", {}, "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g=="], - "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], "@tailwindcss/node": ["@tailwindcss/node@4.1.14", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.6.0", "lightningcss": "1.30.1", "magic-string": "^0.30.19", "source-map-js": "^1.2.1", "tailwindcss": "4.1.14" } }, "sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw=="], @@ -230,32 +220,12 @@ "@types/bun": ["@types/bun@1.3.0", "", { "dependencies": { "bun-types": "1.3.0" } }, "sha512-+lAGCYjXjip2qY375xX/scJeVRmZ5cY0wyHYyCYxNcdEXrQ4AOe3gACgd4iQ8ksOslJtW4VNxBJ8llUwc3a6AA=="], - "@types/d3-array": ["@types/d3-array@3.2.2", "", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="], - - "@types/d3-color": ["@types/d3-color@3.1.3", "", {}, "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A=="], - - "@types/d3-ease": ["@types/d3-ease@3.0.2", "", {}, "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA=="], - - "@types/d3-interpolate": ["@types/d3-interpolate@3.0.4", "", { "dependencies": { "@types/d3-color": "*" } }, "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA=="], - - "@types/d3-path": ["@types/d3-path@3.1.1", "", {}, "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg=="], - - "@types/d3-scale": ["@types/d3-scale@4.0.9", "", { "dependencies": { "@types/d3-time": "*" } }, "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw=="], - - "@types/d3-shape": ["@types/d3-shape@3.1.7", "", { "dependencies": { "@types/d3-path": "*" } }, "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg=="], - - "@types/d3-time": ["@types/d3-time@3.0.4", "", {}, "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g=="], - - "@types/d3-timer": ["@types/d3-timer@3.0.2", "", {}, "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw=="], - "@types/node": ["@types/node@24.7.2", "", { "dependencies": { "undici-types": "~7.14.0" } }, "sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA=="], "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], "@types/react-dom": ["@types/react-dom@19.2.2", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw=="], - "@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], - "aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="], "bun-types": ["bun-types@1.3.0", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-u8X0thhx+yJ0KmkxuEo9HAtdfgCBaM/aI9K90VQcQioAmkVp3SG3FkwWGibUFz3WdXAdcsqOcbU40lK7tbHdkQ=="], @@ -272,40 +242,12 @@ "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], - "d3-array": ["d3-array@3.2.4", "", { "dependencies": { "internmap": "1 - 2" } }, "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg=="], - - "d3-color": ["d3-color@3.1.0", "", {}, "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA=="], - - "d3-ease": ["d3-ease@3.0.1", "", {}, "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w=="], - - "d3-format": ["d3-format@3.1.0", "", {}, "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA=="], - - "d3-interpolate": ["d3-interpolate@3.0.1", "", { "dependencies": { "d3-color": "1 - 3" } }, "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g=="], - - "d3-path": ["d3-path@3.1.0", "", {}, "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ=="], - - "d3-scale": ["d3-scale@4.0.2", "", { "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", "d3-interpolate": "1.2.0 - 3", "d3-time": "2.1.1 - 3", "d3-time-format": "2 - 4" } }, "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ=="], - - "d3-shape": ["d3-shape@3.2.0", "", { "dependencies": { "d3-path": "^3.1.0" } }, "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA=="], - - "d3-time": ["d3-time@3.1.0", "", { "dependencies": { "d3-array": "2 - 3" } }, "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q=="], - - "d3-time-format": ["d3-time-format@4.1.0", "", { "dependencies": { "d3-time": "1 - 3" } }, "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg=="], - - "d3-timer": ["d3-timer@3.0.1", "", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="], - - "decimal.js-light": ["decimal.js-light@2.5.1", "", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="], - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="], "enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="], - "es-toolkit": ["es-toolkit@1.40.0", "", {}, "sha512-8o6w0KFmU0CiIl0/Q/BCEOabF2IJaELM1T2PWj6e8KqzHv1gdx+7JtFnDwOx1kJH/isJ5NwlDG1nCr1HrRF94Q=="], - - "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], @@ -314,12 +256,6 @@ "i18next": ["i18next@25.7.1", "", { "dependencies": { "@babel/runtime": "^7.28.4" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-XbTnkh1yCZWSAZGnA9xcQfHcYNgZs2cNxm+c6v1Ma9UAUGCeJPplRe1ILia6xnDvXBjk0uXU+Z8FYWhA19SKFw=="], - "i18next-resources-to-backend": ["i18next-resources-to-backend@1.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw=="], - - "immer": ["immer@10.1.3", "", {}, "sha512-tmjF/k8QDKydUlm3mZU+tjM6zeq9/fFpPqH9SzWmBnVVKsPBg/V66qsMwb3/Bo90cgUN+ghdVBess+hPsxUyRw=="], - - "internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="], - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], @@ -368,26 +304,12 @@ "react-i18next": ["react-i18next@16.4.0", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "i18next": ">= 25.6.2", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-bxVeBA8Ky2UeItNhF4JRxHCFIrpEJHGFG/mOAa4CR0JkqaDEYSLmlEgmC4Os63SBlZ+E5U0YyrNJOSVl2mtVqQ=="], - "react-is": ["react-is@19.2.0", "", {}, "sha512-x3Ax3kNSMIIkyVYhWPyO09bu0uttcAIoecO/um/rKGQ4EltYWVYtyiGkS/3xMynrbVQdS69Jhlv8FXUEZehlzA=="], - - "react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="], - "react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="], "react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="], "react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="], - "recharts": ["recharts@3.2.1", "", { "dependencies": { "@reduxjs/toolkit": "1.x.x || 2.x.x", "clsx": "^2.1.1", "decimal.js-light": "^2.5.1", "es-toolkit": "^1.39.3", "eventemitter3": "^5.0.1", "immer": "^10.1.1", "react-redux": "8.x.x || 9.x.x", "reselect": "5.1.1", "tiny-invariant": "^1.3.3", "use-sync-external-store": "^1.2.2", "victory-vendor": "^37.0.2" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-0JKwHRiFZdmLq/6nmilxEZl3pqb4T+aKkOkOi/ZISRZwfBhVMgInxzlYU9D4KnCH3KINScLy68m/OvMXoYGZUw=="], - - "recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="], - - "redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], - - "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], - - "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], - "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], @@ -408,8 +330,6 @@ "tar": ["tar@7.5.1", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g=="], - "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], @@ -422,8 +342,6 @@ "use-sync-external-store": ["use-sync-external-store@1.6.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="], - "victory-vendor": ["victory-vendor@37.3.6", "", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="], - "void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="], "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], diff --git a/package.json b/package.json index a94dc5b..9aee045 100644 --- a/package.json +++ b/package.json @@ -31,17 +31,13 @@ "@tauri-apps/plugin-shell": "^2.3.1", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "d3-shape": "^3.2.0", "i18next": "^25.6.0", - "i18next-resources-to-backend": "^1.2.1", "lucide-react": "^0.545.0", "next": "15.5.7", "next-themes": "^0.4.6", "react": "^19.2.0", "react-dom": "^19.2.0", "react-i18next": "^16.0.1", - "recharts": "^3.2.1", - "recharts-scale": "^0.4.5", "sonner": "^2.0.7", "tailwind-merge": "^3.3.1" } diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 7ebfda5..52cc856 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -16,10 +16,10 @@ tauri-plugin-dialog = "2" tauri-plugin-updater = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" -walkdir = "2" sysinfo = "0.36.1" -lazy_static = "1.5" rayon = "1.10" + +[target.'cfg(not(unix))'.dependencies] filesize = "0.2" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 1c33e33..c2b9d6c 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -12,6 +12,7 @@ use rayon::prelude::*; use sysinfo::Disks; #[cfg(unix)] use std::os::unix::fs::MetadataExt; +#[cfg(not(unix))] use filesize::PathExt; use tauri_plugin_updater::UpdaterExt; @@ -425,7 +426,15 @@ async fn scan_directory_streaming(path: String, on_batch: Channel { let limited_root = build_limited_depth_node(&root_node, MAX_DEPTH); send_final_batch(&on_batch, &state, limited_root, disk_info); @@ -487,18 +496,7 @@ async fn delete_files_batch(paths: Vec, on_progress: Channel, on_progress: Channel FileNode { + FileNode { + name: path.file_name().and_then(|n| n.to_str()).unwrap_or("").to_string(), + size: 0, + path: path_str, + children: None, + is_directory: false, + } +} + +fn on_disk_size(path: &Path, link_meta: &fs::Metadata) -> u64 { + #[cfg(unix)] + { + let _ = path; + link_meta.blocks() * 512 + } + #[cfg(not(unix))] + { + let _ = link_meta; + path.size_on_disk().unwrap_or(0) + } +} + +fn path_disk_usage(path: &Path) -> u64 { + let Ok(link_meta) = fs::symlink_metadata(path) else { + return 0; + }; + + if link_meta.file_type().is_symlink() { + let Ok(followed) = fs::metadata(path) else { + return 0; + }; + if followed.is_file() { + on_disk_size(path, &link_meta) + } else if followed.is_dir() { + calculate_dir_size(path) + } else { + 0 + } + } else if link_meta.is_file() { + on_disk_size(path, &link_meta) + } else if link_meta.is_dir() { + calculate_dir_size(path) + } else { + 0 + } +} + fn calculate_dir_size(path: &Path) -> u64 { let mut total_size = 0u64; + let Ok(entries) = fs::read_dir(path) else { + return total_size; + }; - if let Ok(entries) = fs::read_dir(path) { - for entry in entries.flatten() { - let entry_path = entry.path(); - if entry_path.is_file() { - total_size += entry_path.size_on_disk().unwrap_or(0); - } else if entry_path.is_dir() { + for entry in entries.flatten() { + let Ok(link_meta) = entry.metadata() else { + continue; + }; + let entry_path = entry.path(); + + if link_meta.file_type().is_symlink() { + let Ok(followed) = fs::metadata(&entry_path) else { + continue; + }; + if followed.is_file() { + total_size += on_disk_size(&entry_path, &link_meta); + } else if followed.is_dir() { total_size += calculate_dir_size(&entry_path); } + } else if link_meta.is_file() { + total_size += on_disk_size(&entry_path, &link_meta); + } else if link_meta.is_dir() { + total_size += calculate_dir_size(&entry_path); } } @@ -717,199 +776,114 @@ fn scan_directory_recursive( channel: &Channel, state: &ScanState, root_path: &Path, + canonical_root: Option<&Path>, + parent_canonical_ok: bool, ) -> Result { - // Check if scan has been cancelled if state.is_cancelled() { return Err("Scan cancelled".to_string()); } let path_str = path.to_string_lossy().to_string(); - - // Update current scanning path state.set_current_path(&path_str); - - // Send path update if interval reached if state.should_send_path_update() { send_path_update(channel, state); } - // Prevent scanning above the root path to avoid duplicate counting - // Use canonicalized paths for accurate comparison - if let Ok(canonical_root) = fs::canonicalize(root_path) { - if let Ok(canonical_path) = fs::canonicalize(path) { - if !canonical_path.starts_with(&canonical_root) { - return Ok(FileNode { - name: path.file_name().and_then(|n| n.to_str()).unwrap_or("").to_string(), - size: 0, - path: path_str, - children: None, - is_directory: false, - }); + let link_meta = fs::symlink_metadata(path).map_err(|e| e.to_string())?; + let is_link = link_meta.file_type().is_symlink(); + let followed_meta = if is_link { + Some(fs::metadata(path).map_err(|e| e.to_string())?) + } else { + None + }; + let metadata = followed_meta.as_ref().unwrap_or(&link_meta); + + let trust_parent = parent_canonical_ok && !is_link && metadata.is_file(); + let canonical_path = if trust_parent { + None + } else { + fs::canonicalize(path).ok() + }; + + if !trust_parent { + if let Some(canon) = canonical_path.as_ref() { + if let Some(root) = canonical_root { + if !canon.starts_with(root) { + return Ok(zero_leaf(path, path_str)); + } + } + if state.is_in_recursion_stack(canon) { + return Ok(zero_leaf(path, path_str)); } } } - - // Check for circular path using canonicalized path - if let Ok(canonical_path) = fs::canonicalize(path) { - if state.is_in_recursion_stack(&canonical_path) { - return Ok(FileNode { - name: path.file_name().and_then(|n| n.to_str()).unwrap_or("").to_string(), - size: 0, - path: path_str, - children: None, - is_directory: false, - }); - } - } - - let metadata = fs::metadata(path).map_err(|e| e.to_string())?; - - // Check if we've already visited this inode (prevents symlink loops and hard link duplicates) - // Only use inode tracking on Unix systems + #[cfg(unix)] { let inode = metadata.ino(); if state.is_visited_inode(inode) { - return Ok(FileNode { - name: path.file_name().and_then(|n| n.to_str()).unwrap_or("").to_string(), - size: 0, - path: path_str, - children: None, - is_directory: false, - }); + return Ok(zero_leaf(path, path_str)); } state.mark_visited_inode(inode); } - + let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("").to_string(); state.increment_counter(); - // Handle symlinks by following them - if metadata.file_type().is_symlink() { - // Try to follow the symlink - if let Ok(target_path) = fs::read_link(path) { - if let Ok(target_metadata) = fs::metadata(&target_path) { - if target_metadata.is_file() { - // Use filesize to get actual disk usage for symlinked files - let file_size = target_path.size_on_disk().unwrap_or(0); - state.add_size(file_size); - - return Ok(FileNode { - name, - size: file_size, - path: path_str, - children: None, - is_directory: false, - }); - } else if target_metadata.is_dir() { - // For directory symlinks, check if target is above root path or in recursion stack - if let Ok(canonical_root) = fs::canonicalize(root_path) { - if let Ok(canonical_target) = fs::canonicalize(&target_path) { - if !canonical_target.starts_with(&canonical_root) { - return Ok(FileNode { - name, - size: 0, - path: path_str, - children: None, - is_directory: false, - }); - } - } - } - - if let Ok(canonical_target) = fs::canonicalize(&target_path) { - if state.is_in_recursion_stack(&canonical_target) { - return Ok(FileNode { - name, - size: 0, - path: path_str, - children: None, - is_directory: false, - }); - } - } - - // Safe to scan the target directory - return scan_directory_recursive(&target_path, channel, state, root_path); - } - } - } - - // If we can't follow the symlink, return size 0 - return Ok(FileNode { - name, - size: 0, - path: path_str, - children: None, - is_directory: false, - }); - } - if metadata.is_file() { - // Use filesize to get actual disk usage (handles sparse files correctly) - let file_size = path.size_on_disk().unwrap_or(0); - + let file_size = on_disk_size(path, &link_meta); state.add_size(file_size); - - // Don't add files to batch buffer - only send directories to reduce IPC load - let node = FileNode { + return Ok(FileNode { name, size: file_size, path: path_str, children: None, is_directory: false, - }; - - return Ok(node); + }); } - // Scan directory with parallel processing if let Ok(entries) = fs::read_dir(path) { - // Add current directory to recursion stack - if let Ok(canonical_path) = fs::canonicalize(path) { - state.push_to_recursion_stack(&canonical_path); + if let Some(canon) = canonical_path.as_ref() { + state.push_to_recursion_stack(canon); } let entries_vec: Vec<_> = entries.flatten().collect(); - - // No filtering - scan everything - let filtered_entries = entries_vec; - - // For root directory's direct children, send immediate progress updates let is_root_level = path == root_path; + let child_parent_ok = canonical_path.is_some(); - let children: Vec = filtered_entries + let children: Vec = entries_vec .par_iter() .filter_map(|entry| { - // Send progress update before scanning each root-level directory if is_root_level { send_path_update(channel, state); } - scan_directory_recursive(&entry.path(), channel, state, root_path).ok() + scan_directory_recursive( + &entry.path(), + channel, + state, + root_path, + canonical_root, + child_parent_ok, + ) + .ok() }) .collect(); - - // Remove current directory from recursion stack - if let Ok(canonical_path) = fs::canonicalize(path) { - state.pop_from_recursion_stack(&canonical_path); + + if let Some(canon) = canonical_path.as_ref() { + state.pop_from_recursion_stack(canon); } let dir_total_size: u64 = children.iter().map(|c| c.size).sum(); - - // Create directory node for return (with full children tree) let dir_node_with_children = FileNode { - name: name.clone(), + name, size: dir_total_size, - path: path_str.clone(), + path: path_str, children: Some(children), is_directory: true, }; - // Only send compact nodes for direct children of root (depth 1) - // This prevents sending duplicate nested directories if let Some(parent) = path.parent() { if parent == root_path { - // This is a direct child of root - send it with full subtree let compact_dir = to_compact_node(&dir_node_with_children); if state.add_compact_to_buffer(compact_dir) { send_compact_batch(channel, state); diff --git a/src/app/analyze/page.tsx b/src/app/analyze/page.tsx index 9a3e0d9..0efe251 100644 --- a/src/app/analyze/page.tsx +++ b/src/app/analyze/page.tsx @@ -1,6 +1,6 @@ 'use client' -import { useEffect, useState, useRef, Suspense } from 'react' +import { useEffect, useState, useRef, useMemo, useLayoutEffect, Suspense } from 'react' import { useSearchParams, useRouter } from 'next/navigation' import { ArrowLeft, Loader2, X, ArrowUpDown } from 'lucide-react' import { invoke, Channel } from '@tauri-apps/api/core' @@ -11,6 +11,7 @@ import { useVirtualizer } from '@tanstack/react-virtual' import { toast } from 'sonner' import { Toaster } from '@/components/ui/sonner' import { useTranslation } from 'react-i18next' +import { CursorGlow } from '@/components/CursorGlow' interface FileNode { name: string @@ -61,48 +62,27 @@ function hashName(name: string): number { return Math.abs(hash) } -// Generate HSL color based on hash -function generateColorFromHash(name: string, depth: number = 0): string { - const hash = hashName(name) - - // Generate hue from hash (0-360) - const hue = hash % 360 - - // Generate saturation and lightness based on depth - // Deeper levels have lower saturation and higher lightness (lighter colors) - const saturation = Math.max(20, 80 - depth * 15) // 80% to 20% - const lightness = Math.min(80, 40 + depth * 8) // 40% to 80% - - return `hsl(${hue}, ${saturation}%, ${lightness}%)` +type Hsl = { h: number; s: number; l: number } + +function hue2rgb(p: number, q: number, t: number): number { + if (t < 0) t += 1 + if (t > 1) t -= 1 + if (t < 1/6) return p + (q - p) * 6 * t + if (t < 1/2) return q + if (t < 2/3) return p + (q - p) * (2/3 - t) * 6 + return p } -// Mix color with white based on depth -function mixColorWithWhite(baseColor: string, depth: number): string { - // Convert HSL to RGB for mixing - const hslMatch = baseColor.match(/hsl\((\d+),\s*(\d+)%,\s*(\d+)%\)/) - if (!hslMatch) return baseColor - - const hue = parseInt(hslMatch[1]) - const saturation = parseInt(hslMatch[2]) - const lightness = parseInt(hslMatch[3]) - - // Convert HSL to RGB +function mixHslWithWhite(hue: number, saturation: number, lightness: number, depth: number): Hsl { const h = hue / 360 const s = saturation / 100 const l = lightness / 100 - - const hue2rgb = (p: number, q: number, t: number) => { - if (t < 0) t += 1 - if (t > 1) t -= 1 - if (t < 1/6) return p + (q - p) * 6 * t - if (t < 1/2) return q - if (t < 2/3) return p + (q - p) * (2/3 - t) * 6 - return p - } - - let r, g, b + + let r: number + let g: number + let b: number if (s === 0) { - r = g = b = l // achromatic + r = g = b = l } else { const q = l < 0.5 ? l * (1 + s) : l + s - l * s const p = 2 * l - q @@ -110,24 +90,22 @@ function mixColorWithWhite(baseColor: string, depth: number): string { g = hue2rgb(p, q, h) b = hue2rgb(p, q, h - 1/3) } - - // Mix with white based on depth (more white = lighter) - const whiteAmount = Math.min(0.8, depth * 0.2) // Max 80% white at depth 4+ + + const whiteAmount = Math.min(0.8, depth * 0.2) r = r + (1 - r) * whiteAmount g = g + (1 - g) * whiteAmount b = b + (1 - b) * whiteAmount - - // Convert back to HSL + const max = Math.max(r, g, b) const min = Math.min(r, g, b) let newHue = 0 let newSaturation = 0 const newLightness = (max + min) / 2 - + if (max !== min) { const d = max - min newSaturation = newLightness > 0.5 ? d / (2 - max - min) : d / (max + min) - + switch (max) { case r: newHue = (g - b) / d + (g < b ? 6 : 0); break case g: newHue = (b - r) / d + 2; break @@ -135,23 +113,45 @@ function mixColorWithWhite(baseColor: string, depth: number): string { } newHue /= 6 } - - return `hsl(${Math.round(newHue * 360)}, ${Math.round(newSaturation * 100)}%, ${Math.round(newLightness * 100)}%)` + + return { + h: Math.round(newHue * 360), + s: Math.round(newSaturation * 100), + l: Math.round(newLightness * 100), + } } -// Generate color scheme for a name (base color + lighter variants) -function generateColorScheme(name: string): string[] { - const hash = hashName(name) - const hue = hash % 360 - - const schemes = [] - for (let i = 0; i < 5; i++) { - const saturation = Math.max(20, 80 - i * 15) - const lightness = Math.min(80, 40 + i * 8) - schemes.push(`hsl(${hue}, ${saturation}%, ${lightness}%)`) +function formatHsl(h: number, s: number, l: number): string { + return `hsl(${h}, ${s}%, ${l}%)` +} + +const MAX_MIX_DEPTH = 7 + +const MIXED_COLOR_BY_HUE: string[][] = Array.from({ length: 360 }, (_, hue) => { + const chain = [formatHsl(hue, 80, 40)] + let current: Hsl = { h: hue, s: 80, l: 40 } + for (let depth = 1; depth <= MAX_MIX_DEPTH; depth++) { + current = mixHslWithWhite(current.h, current.s, current.l, depth) + chain.push(formatHsl(current.h, current.s, current.l)) } - - return schemes + return chain +}) + +function colorFromHue(hue: number, depth: number): string { + if (depth <= MAX_MIX_DEPTH) return MIXED_COLOR_BY_HUE[hue][depth] + let current: Hsl = { h: hue, s: 80, l: 40 } + for (let d = 1; d <= depth; d++) { + current = mixHslWithWhite(current.h, current.s, current.l, d) + } + return formatHsl(current.h, current.s, current.l) +} + +function generateColorFromHash(name: string, depth: number = 0): string { + const hue = hashName(name) % 360 + if (depth === 0) return MIXED_COLOR_BY_HUE[hue][0] + const saturation = Math.max(20, 80 - depth * 15) + const lightness = Math.min(80, 40 + depth * 8) + return formatHsl(hue, saturation, lightness) } @@ -276,7 +276,6 @@ function AnalyzeContent() { const [isLoading, setIsLoading] = useState(true) const [scanProgress, setScanProgress] = useState<{ currentPath: string; filesScanned: number; scannedSize: number; estimatedTotal: number } | null>(null) const [diskInfo, setDiskInfo] = useState<{ totalSpace: number; availableSpace: number; usedSpace: number } | null>(null) - const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }) const [scanStartTime, setScanStartTime] = useState(null) const [scanElapsedTime, setScanElapsedTime] = useState(0) const [scanCompleteTime, setScanCompleteTime] = useState(null) @@ -303,49 +302,47 @@ function AnalyzeContent() { const svgRef = useRef(null) const [hoveredSectorId, setHoveredSectorId] = useState(null) - const [tooltip, setTooltip] = useState<{ x: number; y: number; content: string; label: string; size: string; icon: any; color: string } | null>(null) + const [tooltip, setTooltip] = useState<{ content: string; label: string; size: string; icon: any; color: string } | null>(null) - // Hover handlers with smart positioning const tooltipRef = useRef(null) + const tooltipMouseRef = useRef({ x: 0, y: 0 }) const listContainerRef = useRef(null) - const calculateTooltipPosition = (mouseX: number, mouseY: number) => { + const positionTooltip = (mouseX: number, mouseY: number) => { + const el = tooltipRef.current + if (!el) return + const offsetX = 8 const offsetY = 8 - - // Use actual tooltip dimensions if available, otherwise use estimates - const tooltipWidth = tooltipRef.current?.offsetWidth || 200 - const tooltipHeight = tooltipRef.current?.offsetHeight || 70 - - // Check if mouse is on left or right half of screen + const tooltipWidth = el.offsetWidth || 200 + const tooltipHeight = el.offsetHeight || 70 const isLeftHalf = mouseX < window.innerWidth / 2 - // Check if mouse is on top or bottom half of screen const isTopHalf = mouseY < window.innerHeight / 2 - - let x = isLeftHalf ? mouseX + offsetX : mouseX - tooltipWidth - offsetX - let y = isTopHalf ? mouseY + offsetY : mouseY - tooltipHeight - offsetY - - return { x, y } + const x = isLeftHalf ? mouseX + offsetX : mouseX - tooltipWidth - offsetX + const y = isTopHalf ? mouseY + offsetY : mouseY - tooltipHeight - offsetY + el.style.left = `${x}px` + el.style.top = `${y}px` } + useLayoutEffect(() => { + if (!tooltip) return + positionTooltip(tooltipMouseRef.current.x, tooltipMouseRef.current.y) + }, [tooltip]) + const handleHover = (sectorId: string, event: React.MouseEvent, label: string, name: string, size: string, icon: any, color: string) => { + tooltipMouseRef.current = { x: event.clientX, y: event.clientY } setHoveredSectorId(sectorId) - const pos = calculateTooltipPosition(event.clientX, event.clientY) - setTooltip({ - x: pos.x, - y: pos.y, - content: name, - label: label, - size: size, - icon: icon, - color: color + setTooltip(prev => { + if (prev && prev.content === name && prev.label === label && prev.size === size && prev.color === color && prev.icon === icon) { + return prev + } + return { content: name, label, size, icon, color } }) + positionTooltip(event.clientX, event.clientY) } const handleMouseMove = (event: React.MouseEvent) => { - if (tooltip) { - const pos = calculateTooltipPosition(event.clientX, event.clientY) - setTooltip(prev => prev ? { ...prev, x: pos.x, y: pos.y } : null) - } + tooltipMouseRef.current = { x: event.clientX, y: event.clientY } + positionTooltip(event.clientX, event.clientY) } const handleLeave = () => { setHoveredSectorId(null) @@ -593,24 +590,6 @@ function AnalyzeContent() { return filterNode(root) || root } - // Mouse tracking for cursor glow effect - useEffect(() => { - let animationFrame: number - - const handleMouseMove = (e: MouseEvent) => { - if (animationFrame) cancelAnimationFrame(animationFrame) - animationFrame = requestAnimationFrame(() => { - setMousePosition({ x: e.clientX, y: e.clientY }) - }) - } - - window.addEventListener('mousemove', handleMouseMove) - return () => { - window.removeEventListener('mousemove', handleMouseMove) - if (animationFrame) cancelAnimationFrame(animationFrame) - } - }, []) - // Sync hover states between chart and file list useEffect(() => { if (!svgRef.current) return @@ -803,7 +782,7 @@ function AnalyzeContent() { depth: number, startAngle: number, endAngle: number, - parentColor?: string + parentHue?: number ) => { if (depth >= maxDepth || !nodes || nodes.length === 0) return @@ -826,11 +805,10 @@ function AnalyzeContent() { return } - // Get color for this node - use parent color mixed with white for deeper levels - const color = depth === 0 + const color = depth === 0 ? generateColorFromHash(node.name, 0) - : parentColor - ? mixColorWithWhite(parentColor, depth) + : parentHue !== undefined + ? colorFromHue(parentHue, depth) : generateColorFromHash(node.name, depth) // Add to current layer @@ -855,7 +833,7 @@ function AnalyzeContent() { depth + 1, currentAngle, nodeEndAngle, - color + depth === 0 ? hashName(node.name) % 360 : parentHue ) } @@ -902,8 +880,8 @@ function AnalyzeContent() { const nodeAngleRange = 360 * proportion const nodeEndAngle = currentAngle + nodeAngleRange - // Generate color based on name hash - const color = generateColorFromHash(node.name, 0) + const hue = hashName(node.name) % 360 + const color = MIXED_COLOR_BY_HUE[hue][0] if (!layers.has(0)) { layers.set(0, []) @@ -921,7 +899,7 @@ function AnalyzeContent() { // Process children if (node.isDirectory && node.children && node.children.length > 0) { - buildHierarchy(node.children, 1, currentAngle, nodeEndAngle, color) + buildHierarchy(node.children, 1, currentAngle, nodeEndAngle, hue) } currentAngle = nodeEndAngle @@ -964,8 +942,8 @@ function AnalyzeContent() { const nodeEndAngle = tinyNodeAngle + nodeAngleRange if (node.isDirectory && node.children && node.children.length > 0) { - const tinyNodeColor = generateColorFromHash(node.name, 0) - buildHierarchy(node.children, 1, tinyNodeAngle, nodeEndAngle, tinyNodeColor) + const tinyHue = hashName(node.name) % 360 + buildHierarchy(node.children, 1, tinyNodeAngle, nodeEndAngle, tinyHue) } tinyNodeAngle = nodeEndAngle @@ -1073,8 +1051,14 @@ function AnalyzeContent() { } // Prepare data (must be before conditional returns) - const layers = prepareMultiLayerData(currentLevel) - const listData = prepareListData(currentLevel) + const layers = useMemo( + () => prepareMultiLayerData(currentLevel), + [currentLevel, diskInfo, data, t] + ) + const listData = useMemo( + () => prepareListData(currentLevel), + [currentLevel, diskInfo, data, sortBy, sortOrder] + ) // Virtual list using TanStack Virtual const rowVirtualizer = useVirtualizer({ @@ -1256,20 +1240,7 @@ function AnalyzeContent() {
- {/* Cursor Follow Glow */} -
-
-
+
@@ -1445,20 +1416,7 @@ function AnalyzeContent() {
- {/* Cursor Follow Glow */} -
-
-
+