image info

This commit is contained in:
2026-04-02 09:49:20 -04:00
parent 27c3065582
commit b109efab98
7 changed files with 56 additions and 19 deletions
+4 -3
View File
@@ -351,6 +351,7 @@ const image = table(
id: t.u64().primaryKey().autoInc(), id: t.u64().primaryKey().autoInc(),
data: t.byteArray(), data: t.byteArray(),
mime_type: t.string(), mime_type: t.string(),
name: t.string().optional(),
}, },
); );
@@ -382,9 +383,9 @@ function validateName(name: string) {
} }
export const upload_image = spacetimedb.reducer( export const upload_image = spacetimedb.reducer(
{ data: t.byteArray(), mimeType: t.string() }, { data: t.byteArray(), mimeType: t.string(), name: t.string().optional() },
(ctx, { data, mimeType }) => { (ctx, { data, mimeType, name }) => {
ctx.db.image.insert({ id: 0n, data, mime_type: mimeType }); ctx.db.image.insert({ id: 0n, data, mime_type: mimeType, name });
}, },
); );
+35
View File
@@ -8,6 +8,12 @@
onClose(); onClose();
} }
} }
const formatSize = (bytes: number) => {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
};
</script> </script>
<svelte:window onkeydown={handleKeydown} /> <svelte:window onkeydown={handleKeydown} />
@@ -15,6 +21,13 @@
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div class="image-viewer-overlay" onclick={onClose}> <div class="image-viewer-overlay" onclick={onClose}>
<div class="image-viewer-info">
<div class="info-filename">{image.name || "Untitled Image"}</div>
<div class="info-details">
{image.mimeType}{formatSize(image.data.length)}
</div>
</div>
<button <button
class="image-viewer-close" class="image-viewer-close"
onclick={(e) => { e.stopPropagation(); onClose(); }} onclick={(e) => { e.stopPropagation(); onClose(); }}
@@ -40,12 +53,34 @@
bottom: 0; bottom: 0;
background-color: rgba(0, 0, 0, 0.9); background-color: rgba(0, 0, 0, 0.9);
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
z-index: 3000; z-index: 3000;
cursor: zoom-out; cursor: zoom-out;
} }
.image-viewer-info {
position: absolute;
top: 20px;
left: 20px;
color: white;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
z-index: 3100;
pointer-events: none;
}
.info-filename {
font-weight: bold;
font-size: 1.1rem;
margin-bottom: 4px;
}
.info-details {
font-size: 0.85rem;
opacity: 0.8;
}
.image-viewer-content { .image-viewer-content {
position: relative; position: relative;
max-width: 90vw; max-width: 90vw;
+5 -5
View File
@@ -16,7 +16,7 @@
const chat = getContext<ChatService>("chat"); const chat = getContext<ChatService>("chat");
let messageText = $state(""); let messageText = $state("");
let stagedImages = $state<{ data: Uint8Array; mimeType: string; previewUrl: string }[]>([]); let stagedImages = $state<{ data: Uint8Array; mimeType: string; previewUrl: string; name: string }[]>([]);
async function handlePaste(e: ClipboardEvent) { async function handlePaste(e: ClipboardEvent) {
if (!isFullyAuthenticated) return; if (!isFullyAuthenticated) return;
@@ -29,16 +29,16 @@
if (!file) continue; if (!file) continue;
try { try {
const { data, mimeType } = await optimizeImage(file); const { data, mimeType, name } = await optimizeImage(file);
const previewUrl = URL.createObjectURL(new Blob([data], { type: mimeType })); const previewUrl = URL.createObjectURL(new Blob([data], { type: mimeType }));
stagedImages.push({ data, mimeType, previewUrl }); stagedImages.push({ data, mimeType, previewUrl, name });
} catch (err) { } catch (err) {
console.error("Failed to optimize image:", err); console.error("Failed to optimize image:", err);
// Fallback to raw if optimization fails // Fallback to raw if optimization fails
const buffer = await file.arrayBuffer(); const buffer = await file.arrayBuffer();
const data = new Uint8Array(buffer); const data = new Uint8Array(buffer);
const previewUrl = URL.createObjectURL(new Blob([data], { type: file.type })); const previewUrl = URL.createObjectURL(new Blob([data], { type: file.type }));
stagedImages.push({ data, mimeType: file.type, previewUrl }); stagedImages.push({ data, mimeType: file.type, previewUrl, name: file.name });
} }
} }
} }
@@ -63,7 +63,7 @@
const imageIds: bigint[] = []; const imageIds: bigint[] = [];
for (const staged of currentStaged) { for (const staged of currentStaged) {
const beforeCount = chat.images.length; const beforeCount = chat.images.length;
chat.uploadImage(staged.data, staged.mimeType); chat.uploadImage(staged.data, staged.mimeType, staged.name);
// Polling for the new image ID // Polling for the new image ID
const imageId = await new Promise<bigint>((resolve) => { const imageId = await new Promise<bigint>((resolve) => {
@@ -16,7 +16,7 @@
const chat = getContext<ChatService>("chat"); const chat = getContext<ChatService>("chat");
let threadMessageText = $state(""); let threadMessageText = $state("");
let stagedImages = $state<{ data: Uint8Array; mimeType: string; previewUrl: string }[]>([]); let stagedImages = $state<{ data: Uint8Array; mimeType: string; previewUrl: string; name: string }[]>([]);
async function handlePaste(e: ClipboardEvent) { async function handlePaste(e: ClipboardEvent) {
if (!isFullyAuthenticated) return; if (!isFullyAuthenticated) return;
@@ -29,15 +29,15 @@
if (!file) continue; if (!file) continue;
try { try {
const { data, mimeType } = await optimizeImage(file); const { data, mimeType, name } = await optimizeImage(file);
const previewUrl = URL.createObjectURL(new Blob([data], { type: mimeType })); const previewUrl = URL.createObjectURL(new Blob([data], { type: mimeType }));
stagedImages.push({ data, mimeType, previewUrl }); stagedImages.push({ data, mimeType, previewUrl, name });
} catch (err) { } catch (err) {
console.error("Failed to optimize image:", err); console.error("Failed to optimize image:", err);
const buffer = await file.arrayBuffer(); const buffer = await file.arrayBuffer();
const data = new Uint8Array(buffer); const data = new Uint8Array(buffer);
const previewUrl = URL.createObjectURL(new Blob([data], { type: file.type })); const previewUrl = URL.createObjectURL(new Blob([data], { type: file.type }));
stagedImages.push({ data, mimeType: file.type, previewUrl }); stagedImages.push({ data, mimeType: file.type, previewUrl, name: file.name });
} }
} }
} }
@@ -63,7 +63,7 @@
const imageIds: bigint[] = []; const imageIds: bigint[] = [];
for (const staged of currentStaged) { for (const staged of currentStaged) {
const beforeCount = chat.images.length; const beforeCount = chat.images.length;
chat.uploadImage(staged.data, staged.mimeType); chat.uploadImage(staged.data, staged.mimeType, staged.name);
const imageId = await new Promise<bigint>((resolve) => { const imageId = await new Promise<bigint>((resolve) => {
const checkInterval = setInterval(() => { const checkInterval = setInterval(() => {
+2 -2
View File
@@ -304,8 +304,8 @@ export class ChatService {
this.#msg.toggleReaction(messageId, emoji); this.#msg.toggleReaction(messageId, emoji);
}; };
uploadImage = async (data: Uint8Array, mimeType: string) => { uploadImage = async (data: Uint8Array, mimeType: string, name?: string) => {
this.#msg.uploadImage(data, mimeType); this.#msg.uploadImage(data, mimeType, name);
}; };
handleSendThreadMessage = (e: Event) => { handleSendThreadMessage = (e: Event) => {
+2 -2
View File
@@ -78,8 +78,8 @@ export class MessagingService {
} }
}; };
uploadImage = async (data: Uint8Array, mimeType: string) => { uploadImage = async (data: Uint8Array, mimeType: string, name?: string) => {
this.#uploadImageReducer({ data, mimeType }); this.#uploadImageReducer({ data, mimeType, name });
}; };
toggleReaction = (messageId: bigint, emoji: string) => { toggleReaction = (messageId: bigint, emoji: string) => {
+3 -2
View File
@@ -18,7 +18,7 @@ export const formatTime = (ts: any) => {
return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" }); return date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
}; };
export const optimizeImage = async (file: File): Promise<{ data: Uint8Array, mimeType: string }> => { export const optimizeImage = async (file: File): Promise<{ data: Uint8Array, mimeType: string, name: string }> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const img = new Image(); const img = new Image();
const reader = new FileReader(); const reader = new FileReader();
@@ -60,7 +60,8 @@ export const optimizeImage = async (file: File): Promise<{ data: Uint8Array, mim
const buffer = await blob.arrayBuffer(); const buffer = await blob.arrayBuffer();
resolve({ resolve({
data: new Uint8Array(buffer), data: new Uint8Array(buffer),
mimeType: "image/webp" mimeType: "image/webp",
name: file.name
}); });
}, "image/webp", 0.8); }, "image/webp", 0.8);
}; };