use thumbhash instead
This commit is contained in:
parent
972c8930cd
commit
ae74906217
5 changed files with 107 additions and 18 deletions
31
bun.lock
31
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=="],
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
54
src/components/dashboard/Thumbhash.tsx
Normal file
54
src/components/dashboard/Thumbhash.tsx
Normal file
|
@ -0,0 +1,54 @@
|
|||
import { component$, QwikIntrinsicElements, useSignal, useTask$ } from "@builder.io/qwik";
|
||||
import { thumbHashToRGBA } from "thumbhash";
|
||||
|
||||
export default component$<QwikIntrinsicElements['img'] & {
|
||||
hash: string;
|
||||
width: number;
|
||||
height: number;
|
||||
}>(({ hash, width, height, class: cls, ...otherProps }) => {
|
||||
const dataUrl = useSignal<string | null>(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 ? (
|
||||
<><img
|
||||
src={dataUrl.value}
|
||||
width={width}
|
||||
height={height}
|
||||
class={["object-cover", cls]}
|
||||
alt="blurhash placeholder"
|
||||
{...otherProps}
|
||||
/></>
|
||||
) : null;
|
||||
});
|
|
@ -5,6 +5,9 @@ export type StereoFile = {
|
|||
Size: number;
|
||||
CreatedAt: string;
|
||||
Mime: string;
|
||||
Hash?: string;
|
||||
Width?: number;
|
||||
Height?: number;
|
||||
}
|
||||
|
||||
export type StereoUser = {
|
||||
|
|
|
@ -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 (
|
||||
<div class="w-full h-max object-cover flex-grow relative overflow-clip">
|
||||
{fileType.value === "image" && (
|
||||
// eslint-disable-next-line qwik/jsx-img
|
||||
<img
|
||||
width={400}
|
||||
src={`/api/${file.ID}`}
|
||||
alt={file.Name}
|
||||
class="w-full min-h-30 object-cover flex-grow hover:scale-[102.5%] transition-all duration-500"
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
aspectRatio: file.Width && file.Height ? `${file.Width} / ${file.Height}` : "16 / 9",
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{file.Hash && !loaded.value && (
|
||||
<Thumbhash hash={file.Hash} width={file.Width || 0} height={file.Height || 0} class="absolute w-full h-full" />
|
||||
)}
|
||||
|
||||
<img
|
||||
width={400}
|
||||
src={`/api/${file.ID}`}
|
||||
alt={file.Name}
|
||||
class="w-full min-h-30 object-cover flex-grow hover:scale-[102.5%] transition-all duration-500"
|
||||
onLoad$={() => loaded.value = true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{fileType.value === "video" && (
|
||||
<video
|
||||
|
@ -181,7 +197,7 @@ const Files = component$(() => {
|
|||
{hasMore.value && (
|
||||
<div
|
||||
ref={sentinel}
|
||||
class="absolute bottom-0 left-0 h-56 flex items-center justify-center w-full"
|
||||
class="absolute bottom-0 left-0 h-1/2 flex items-center justify-center w-full"
|
||||
>
|
||||
{/* {loadingMore.value && (
|
||||
loading spinner?
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue