make loading & no file screen better & remove base64 (fy hex)
This commit is contained in:
parent
cfc724ecce
commit
a5ecea4bbf
5 changed files with 86 additions and 89 deletions
|
@ -9,12 +9,6 @@ type FileProps = {
|
|||
file: StereoFile;
|
||||
}
|
||||
|
||||
const getBase64Size = (b: string) => {
|
||||
if (!b) return 0;
|
||||
const padding = (b.match(/=+$/) || [""])[0].length;
|
||||
return Math.floor((b.length * 3) / 4) - padding;
|
||||
};
|
||||
|
||||
const formatSize = (bytes: number) => {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
|
@ -31,105 +25,95 @@ export default component$(({ file }: FileProps) => {
|
|||
files.value = await api.list();
|
||||
});
|
||||
|
||||
const addFileToClipboard = $(async (base64: string) => {
|
||||
if (!base64) return;
|
||||
try {
|
||||
let mime = "image/png";
|
||||
if (file.ID.endsWith(".png")) mime = "image/png";
|
||||
else if (file.ID.endsWith(".jpg") || file.ID.endsWith(".jpeg")) mime = "image/jpeg";
|
||||
else if (file.ID.endsWith(".gif")) mime = "image/gif";
|
||||
if (!mime.startsWith("image/")) {
|
||||
alert("Clipboard copy is only supported for images in your browser.");
|
||||
return;
|
||||
}
|
||||
const addFileToClipboard = $(async () => {
|
||||
const response = await api.file(file.ID);
|
||||
const data = await response.blob();
|
||||
let mime = data.type || "application/octet-stream";
|
||||
let clip;
|
||||
|
||||
let pngBlob: Blob;
|
||||
if (mime !== "image/png") {
|
||||
const img = new window.Image();
|
||||
img.src = `data:${mime};base64,${base64}`;
|
||||
await new Promise((res, rej) => {
|
||||
img.onload = res;
|
||||
img.onerror = rej;
|
||||
});
|
||||
if (navigator.clipboard && window.ClipboardItem) {
|
||||
if (mime === "image/jpeg" || mime === "image/jpg") {
|
||||
const img = document.createElement("img");
|
||||
img.src = URL.createObjectURL(data);
|
||||
await new Promise((res) => (img.onload = res));
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = img.width;
|
||||
canvas.height = img.height;
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx?.drawImage(img, 0, 0);
|
||||
pngBlob = await new Promise<Blob>((resolve, reject) => {
|
||||
canvas.toBlob(blob => {
|
||||
if (blob) resolve(blob);
|
||||
else reject(new Error("Failed to convert image to PNG"));
|
||||
}, "image/png");
|
||||
});
|
||||
const png = await new Promise<Blob>((resolve) =>
|
||||
canvas.toBlob((b) => resolve(b!), "image/png")
|
||||
);
|
||||
mime = "image/png";
|
||||
clip = new ClipboardItem({ [mime]: png });
|
||||
} else {
|
||||
const binary = atob(base64);
|
||||
const len = binary.length;
|
||||
const bytes = new Uint8Array(len);
|
||||
for (let i = 0; i < len; i++) {
|
||||
bytes[i] = binary.charCodeAt(i);
|
||||
}
|
||||
pngBlob = new Blob([bytes], { type: "image/png" });
|
||||
clip = new ClipboardItem({ [mime]: data });
|
||||
}
|
||||
|
||||
const item = new ClipboardItem({ "image/png": pngBlob });
|
||||
await navigator.clipboard.write([item]);
|
||||
alert("Image copied to clipboard as PNG!");
|
||||
} catch (error) {
|
||||
console.error("Failed to copy file to clipboard:", error);
|
||||
alert("Failed to copy file to clipboard.");
|
||||
try {
|
||||
await navigator.clipboard.write([clip]);
|
||||
alert("File added to clipboard successfully!");
|
||||
} catch (error) {
|
||||
console.error("Failed to add file to clipboard:", error);
|
||||
alert("Failed to add file to clipboard. Please try again.");
|
||||
}
|
||||
} else {
|
||||
alert("Clipboard API not supported in this browser.");
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div class="rounded-xl bg-neutral-900 flex flex-col group overflow-hidden hover:bg-neutral-800 transition-all duration-200">
|
||||
<div class="relative">
|
||||
<a href={`/api/${file.ID}`} target="_blank">
|
||||
{ file.Base64 && (file.ID.endsWith(".png") || file.ID.endsWith(".jpg") || file.ID.endsWith(".jpeg")) && (
|
||||
<img
|
||||
width={400}
|
||||
height={300}
|
||||
src={`data:image/png;base64,${file.Base64}`}
|
||||
alt={file.ID}
|
||||
class="w-full h-60 object-cover bg-neutral-800 flex-grow"
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
<div class="rounded-xl bg-neutral-900 flex flex-col group overflow-hidden hover:bg-neutral-800 transition-all duration-200">
|
||||
<div class="relative">
|
||||
<a href={`/api/${file.ID}`} target="_blank">
|
||||
{ (file.ID.endsWith(".png") || file.ID.endsWith(".jpg") || file.ID.endsWith(".jpeg")) && (
|
||||
<img
|
||||
width={400}
|
||||
height={300}
|
||||
src={`/api/${file.ID}`}
|
||||
alt={file.ID}
|
||||
class="w-full h-60 object-cover bg-neutral-800 flex-grow"
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
|
||||
<div class="absolute bottom-2 right-2 gap-2 z-10 group-hover:flex hidden duration-200 transition-all">
|
||||
<a
|
||||
class="bg-neutral-600/40 backdrop-blur-lg hover:bg-neutral-600/70 transition-all duration-200 text-white p-2 rounded-lg"
|
||||
href={`/api/${file.ID}`}
|
||||
target="_blank"
|
||||
>
|
||||
<SolarDownloadMinimalisticBold class="w-6 h-6"/>
|
||||
</a>
|
||||
|
||||
<button
|
||||
class="bg-green-600/50 backdrop-blur-lg hover:bg-green-600/75 transition-all duration-200 text-white p-2 rounded-lg"
|
||||
onClick$={async () => await addFileToClipboard(file.Base64)}
|
||||
>
|
||||
<SolarClipboardAddBold class="w-6 h-6"/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="bg-red-600/50 backdrop-blur-lg hover:bg-red-600/75 transition-all duration-200 text-white p-2 rounded-lg"
|
||||
onClick$={async () => await deleteFile(file.ID)}
|
||||
>
|
||||
<SolarTrashBin2Bold class="w-6 h-6"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="absolute bottom-2 right-2 gap-2 z-10 group-hover:flex hidden duration-200 transition-all">
|
||||
<a
|
||||
class="bg-neutral-600/40 backdrop-blur-lg hover:bg-neutral-600/75 transition-all duration-200 text-white p-2 rounded-lg active:scale-95"
|
||||
href={`/api/${file.ID}`}
|
||||
target="_blank"
|
||||
>
|
||||
<SolarDownloadMinimalisticBold class="w-6 h-6"/>
|
||||
</a>
|
||||
|
||||
<button
|
||||
class="bg-green-600/50 backdrop-blur-lg hover:bg-green-600/75 transition-all duration-200 text-white p-2 rounded-lg active:scale-95"
|
||||
onClick$={async () => await addFileToClipboard()}
|
||||
>
|
||||
<SolarClipboardAddBold class="w-6 h-6"/>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="bg-red-600/50 backdrop-blur-lg hover:bg-red-600/75 transition-all duration-200 text-white p-2 rounded-lg active:scale-95"
|
||||
onClick$={async () => await deleteFile(file.ID)}
|
||||
>
|
||||
<SolarTrashBin2Bold class="w-6 h-6"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="p-4 flex-grow-0 text-center">
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center items-center h-full">
|
||||
<div class="p-4 flex flex-col w-full text-center">
|
||||
<p class="text-lg font-semibold text-white w-full truncate">
|
||||
{ file.ID.split("_").slice(1).join("_") || "Untitled" }
|
||||
</p>
|
||||
<div class="flex gap-1 text-sm text-neutral-400 items-center justify-center">
|
||||
<span>{ formatSize(getBase64Size(file.Base64)) }</span>
|
||||
<div class="flex gap-1 text-sm text-neutral-500 items-center justify-center">
|
||||
<span>{ formatSize(file.Size) }</span>
|
||||
<span class="text-neutral-600">•</span>
|
||||
<p>Uploaded on { new Date(file.CreatedAt).toLocaleDateString() }</p>
|
||||
<span>Uploaded on { new Date(file.CreatedAt).toLocaleDateString() }</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
|
@ -31,4 +31,9 @@ export function SolarDownloadMinimalisticBold(props: QwikIntrinsicElements['svg'
|
|||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props} key={key}><path fill="currentColor" d="M12.554 16.506a.75.75 0 0 1-1.107 0l-4-4.375a.75.75 0 0 1 1.107-1.012l2.696 2.95V3a.75.75 0 0 1 1.5 0v11.068l2.697-2.95a.75.75 0 1 1 1.107 1.013z" /><path fill="currentColor" d="M3.75 15a.75.75 0 0 0-1.5 0v.055c0 1.367 0 2.47.117 3.337c.12.9.38 1.658.981 2.26c.602.602 1.36.86 2.26.982c.867.116 1.97.116 3.337.116h6.11c1.367 0 2.47 0 3.337-.116c.9-.122 1.658-.38 2.26-.982s.86-1.36.982-2.26c.116-.867.116-1.97.116-3.337V15a.75.75 0 0 0-1.5 0c0 1.435-.002 2.436-.103 3.192c-.099.734-.28 1.122-.556 1.399c-.277.277-.665.457-1.4.556c-.755.101-1.756.103-3.191.103H9c-1.435 0-2.437-.002-3.192-.103c-.734-.099-1.122-.28-1.399-.556c-.277-.277-.457-.665-.556-1.4c-.101-.755-.103-1.756-.103-3.191" /></svg>
|
||||
)
|
||||
}
|
||||
export function SvgSpinnersBarsRotateFade(props: QwikIntrinsicElements['svg'], key: string) {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" {...props} key={key}><g><rect width="2" height="5" x="11" y="1" fill="currentColor" opacity=".14" /><rect width="2" height="5" x="11" y="1" fill="currentColor" opacity=".29" transform="rotate(30 12 12)" /><rect width="2" height="5" x="11" y="1" fill="currentColor" opacity=".43" transform="rotate(60 12 12)" /><rect width="2" height="5" x="11" y="1" fill="currentColor" opacity=".57" transform="rotate(90 12 12)" /><rect width="2" height="5" x="11" y="1" fill="currentColor" opacity=".71" transform="rotate(120 12 12)" /><rect width="2" height="5" x="11" y="1" fill="currentColor" opacity=".86" transform="rotate(150 12 12)" /><rect width="2" height="5" x="11" y="1" fill="currentColor" transform="rotate(180 12 12)" /><animateTransform attributeName="transform" calcMode="discrete" dur="0.75s" repeatCount="indefinite" type="rotate" values="0 12 12;30 12 12;60 12 12;90 12 12;120 12 12;150 12 12;180 12 12;210 12 12;240 12 12;270 12 12;300 12 12;330 12 12;360 12 12" /></g></svg>
|
||||
)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue