diff --git a/bun.lock b/bun.lock index 9ac6a3b..e878566 100644 --- a/bun.lock +++ b/bun.lock @@ -6,27 +6,30 @@ "dependencies": { "@types/aos": "^3.0.7", "aos": "^3.0.0-beta.6", - "ky": "^1.8.1", + "buffer": "^6.0.3", + "fast-blurhash": "^1.1.4", + "ky": "^1.8.2", "nanostores": "^1.0.1", "tailwind-scrollbar": "^4.0.2", + "thumbhash": "^0.1.1", }, "devDependencies": { - "@builder.io/qwik": "^1.14.1", - "@builder.io/qwik-city": "^1.14.1", + "@builder.io/qwik": "^1.15.0", + "@builder.io/qwik-city": "^1.15.0", "@eslint/js": "latest", - "@tailwindcss/vite": "^4.0.0", + "@tailwindcss/vite": "^4.1.11", "@types/node": "20.14.11", "eslint": "9.25.1", - "eslint-plugin-qwik": "^1.14.1", + "eslint-plugin-qwik": "^1.15.0", "globals": "16.0.0", "prettier": "3.3.3", - "prettier-plugin-tailwindcss": "^0.6.11", - "tailwindcss": "^4.0.0", + "prettier-plugin-tailwindcss": "^0.6.14", + "tailwindcss": "^4.1.11", "typescript": "5.4.5", "typescript-eslint": "8.26.1", - "undici": "*", + "undici": "^7.12.0", "vite": "5.3.5", - "vite-tsconfig-paths": "^4.2.1", + "vite-tsconfig-paths": "^4.3.2", }, }, }, @@ -323,12 +326,16 @@ "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + "call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="], "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], @@ -475,6 +482,8 @@ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + "fast-blurhash": ["fast-blurhash@1.1.4", "", {}, "sha512-xeH121M027hgWHHhHWYYjUmMKl8vCH3PPkXk439ixsP8Bvb/r3UFqg12oMSToD/aSAw8EE6XiTdfZ6M5jaLfzg=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -545,6 +554,8 @@ "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "imagetools-core": ["imagetools-core@7.1.0", "", {}, "sha512-8Aa4NecBBGmTkaAUjcuRYgTPKHCsBEWYmCnvKCL6/bxedehtVVFyZPdXe8DD0Nevd6UWBq85ifUaJ8498lgqNQ=="], @@ -915,6 +926,8 @@ "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="], + "thumbhash": ["thumbhash@0.1.1", "", {}, "sha512-kH5pKeIIBPQXAOni2AiY/Cu/NKdkFREdpH+TLdM0g6WA7RriCv0kPLgP731ady67MhTAqrVG/4mnEeibVuCJcg=="], + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], diff --git a/package.json b/package.json index 58a2349..a54b50c 100644 --- a/package.json +++ b/package.json @@ -44,8 +44,11 @@ "dependencies": { "@types/aos": "^3.0.7", "aos": "^3.0.0-beta.6", + "buffer": "^6.0.3", + "fast-blurhash": "^1.1.4", "ky": "^1.8.2", "nanostores": "^1.0.1", - "tailwind-scrollbar": "^4.0.2" + "tailwind-scrollbar": "^4.0.2", + "thumbhash": "^0.1.1" } } diff --git a/src/components/dashboard/Thumbhash.tsx b/src/components/dashboard/Thumbhash.tsx new file mode 100644 index 0000000..77cc37d --- /dev/null +++ b/src/components/dashboard/Thumbhash.tsx @@ -0,0 +1,54 @@ +import { component$, QwikIntrinsicElements, useSignal, useTask$ } from "@builder.io/qwik"; +import { thumbHashToRGBA } from "thumbhash"; + +export default component$(({ hash, width, height, class: cls, ...otherProps }) => { + const dataUrl = useSignal(null); + + useTask$(() => { + if (!hash) return; + + // Decode base64 to Uint8Array (browser-safe) + const binary = atob(hash); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + + try { + const image = thumbHashToRGBA(bytes); + console.log("pixels", image); + + const canvas = document.createElement("canvas"); + canvas.width = image.w; + canvas.height = image.h; + const ctx = canvas.getContext("2d"); + if (!ctx) { + console.error("failed to get canvas context"); + return; + } + + const imageData = ctx.createImageData(image.w, image.h); + imageData.data.set(image.rgba); + ctx.putImageData(imageData, 0, 0); + ctx.drawImage(canvas, 0, 0); + dataUrl.value = canvas.toDataURL("image/png"); + } catch (e) { + console.error("ThumbHash decode error:", e); + } + }); + + return dataUrl.value ? ( + <>blurhash placeholder + ) : null; +}); \ No newline at end of file diff --git a/src/lib/types.ts b/src/lib/types.ts index 72bec61..a321fe1 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -5,6 +5,9 @@ export type StereoFile = { Size: number; CreatedAt: string; Mime: string; + Hash?: string; + Width?: number; + Height?: number; } export type StereoUser = { diff --git a/src/routes/dashboard/index.tsx b/src/routes/dashboard/index.tsx index fe68b56..aec2a66 100644 --- a/src/routes/dashboard/index.tsx +++ b/src/routes/dashboard/index.tsx @@ -1,8 +1,10 @@ +/* eslint-disable qwik/jsx-img */ /* eslint-disable qwik/no-use-visible-task */ import { component$, Signal, useSignal, useTask$, useVisibleTask$ } from "@builder.io/qwik"; import { routeLoader$, type DocumentHead } from "@builder.io/qwik-city"; import Actionbar from "~/components/dashboard/Actionbar"; import Settings from "~/components/dashboard/Settings"; +import Thumbhash from "~/components/dashboard/Thumbhash"; import Titlebar from "~/components/dashboard/Titlebar"; import { useNanostore$ } from "~/hooks/nanostores"; // import Dropzone from "~/components/Dropzone"; @@ -52,16 +54,30 @@ const Files = component$(() => { else fileType.value = "other"; }); + const loaded = useSignal(false); return (
{fileType.value === "image" && ( - // eslint-disable-next-line qwik/jsx-img - {file.Name} +
+ {file.Hash && !loaded.value && ( + + )} + + {file.Name} loaded.value = true} + /> +
)} {fileType.value === "video" && (