make loading & no file screen better & remove base64 (fy hex)

This commit is contained in:
grngxd 2025-06-08 18:48:15 +01:00
parent cfc724ecce
commit a5ecea4bbf
5 changed files with 86 additions and 89 deletions

View file

@ -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>
)
})

View file

@ -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>
)
}

View file

@ -16,6 +16,7 @@ export const apiClient = ky.create({
});
// TODO: make wrapper for apiclient fr
export const api = {
file: async (file_id: string) => await apiClient.get(file_id),
list: async () => await apiClient.get('list').json<StereoFile[]>(),
upload: async (file: File) => {
const formData = new FormData();

View file

@ -3,5 +3,5 @@ export type StereoFile = {
Path: string;
Owner: string;
CreatedAt: string;
Base64: string;
Size: number;
}

View file

@ -2,6 +2,7 @@ import { component$, useSignal, useVisibleTask$ } from "@builder.io/qwik";
import type { DocumentHead } from "@builder.io/qwik-city";
import Controlbar from "~/components/Controlbar";
import File from "~/components/File";
import { SolarUploadLinear, SvgSpinnersBarsRotateFade } from "~/components/Icons";
import { useNanostore$ } from "~/hooks/nanostores";
import { api } from "~/lib/api";
import { OAUTH_LINK } from "~/lib/constants";
@ -25,13 +26,19 @@ export default component$(() => {
<>
<Controlbar />
<a href={OAUTH_LINK}>oauth</a>
{/* TODO: make ts better :broken_heart: */}
{!loaded.value ? (
<p>loading</p>
<div class="absolute w-full h-screen flex justify-center items-center flex-col">
<p class="text-gray-500 text-8xl font-bold"><SvgSpinnersBarsRotateFade /></p>
<p class="text-gray-700 text-2xl font-light italic">loading your files...</p>
<span class="text-gray-700 text-lg font-light flex gap-[0.5ch] items-center">please wait <span class="animate-spin"></span></span>
</div>
) : (
files.value.length === 0 ? (
<p> no files found fr </p>
<div class="absolute w-full h-screen flex justify-center items-center flex-col">
<p class="text-gray-500 text-8xl font-bold">{"┻━┻︵ \\(°□°)/ ︵ ┻━┻"}</p>
<p class="text-gray-700 text-2xl font-light italic">you haven't uploaded any files yet!</p>
<span class="text-gray-700 text-lg font-light flex gap-[0.5ch] items-center">click the <span><SolarUploadLinear /></span> button to get started</span>
</div>
)
: (
<div class="grid grid-cols-4 gap-4 p-4 mb-18">