image info
This commit is contained in:
@@ -351,6 +351,7 @@ const image = table(
|
||||
id: t.u64().primaryKey().autoInc(),
|
||||
data: t.byteArray(),
|
||||
mime_type: t.string(),
|
||||
name: t.string().optional(),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -382,9 +383,9 @@ function validateName(name: string) {
|
||||
}
|
||||
|
||||
export const upload_image = spacetimedb.reducer(
|
||||
{ data: t.byteArray(), mimeType: t.string() },
|
||||
(ctx, { data, mimeType }) => {
|
||||
ctx.db.image.insert({ id: 0n, data, mime_type: mimeType });
|
||||
{ data: t.byteArray(), mimeType: t.string(), name: t.string().optional() },
|
||||
(ctx, { data, mimeType, name }) => {
|
||||
ctx.db.image.insert({ id: 0n, data, mime_type: mimeType, name });
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -8,6 +8,12 @@
|
||||
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>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
@@ -15,6 +21,13 @@
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<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
|
||||
class="image-viewer-close"
|
||||
onclick={(e) => { e.stopPropagation(); onClose(); }}
|
||||
@@ -40,12 +53,34 @@
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 3000;
|
||||
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 {
|
||||
position: relative;
|
||||
max-width: 90vw;
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
const chat = getContext<ChatService>("chat");
|
||||
|
||||
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) {
|
||||
if (!isFullyAuthenticated) return;
|
||||
@@ -29,16 +29,16 @@
|
||||
if (!file) continue;
|
||||
|
||||
try {
|
||||
const { data, mimeType } = await optimizeImage(file);
|
||||
const { data, mimeType, name } = await optimizeImage(file);
|
||||
const previewUrl = URL.createObjectURL(new Blob([data], { type: mimeType }));
|
||||
stagedImages.push({ data, mimeType, previewUrl });
|
||||
stagedImages.push({ data, mimeType, previewUrl, name });
|
||||
} catch (err) {
|
||||
console.error("Failed to optimize image:", err);
|
||||
// Fallback to raw if optimization fails
|
||||
const buffer = await file.arrayBuffer();
|
||||
const data = new Uint8Array(buffer);
|
||||
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[] = [];
|
||||
for (const staged of currentStaged) {
|
||||
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
|
||||
const imageId = await new Promise<bigint>((resolve) => {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
const chat = getContext<ChatService>("chat");
|
||||
|
||||
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) {
|
||||
if (!isFullyAuthenticated) return;
|
||||
@@ -29,15 +29,15 @@
|
||||
if (!file) continue;
|
||||
|
||||
try {
|
||||
const { data, mimeType } = await optimizeImage(file);
|
||||
const { data, mimeType, name } = await optimizeImage(file);
|
||||
const previewUrl = URL.createObjectURL(new Blob([data], { type: mimeType }));
|
||||
stagedImages.push({ data, mimeType, previewUrl });
|
||||
stagedImages.push({ data, mimeType, previewUrl, name });
|
||||
} catch (err) {
|
||||
console.error("Failed to optimize image:", err);
|
||||
const buffer = await file.arrayBuffer();
|
||||
const data = new Uint8Array(buffer);
|
||||
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[] = [];
|
||||
for (const staged of currentStaged) {
|
||||
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 checkInterval = setInterval(() => {
|
||||
|
||||
@@ -304,8 +304,8 @@ export class ChatService {
|
||||
this.#msg.toggleReaction(messageId, emoji);
|
||||
};
|
||||
|
||||
uploadImage = async (data: Uint8Array, mimeType: string) => {
|
||||
this.#msg.uploadImage(data, mimeType);
|
||||
uploadImage = async (data: Uint8Array, mimeType: string, name?: string) => {
|
||||
this.#msg.uploadImage(data, mimeType, name);
|
||||
};
|
||||
|
||||
handleSendThreadMessage = (e: Event) => {
|
||||
|
||||
@@ -78,8 +78,8 @@ export class MessagingService {
|
||||
}
|
||||
};
|
||||
|
||||
uploadImage = async (data: Uint8Array, mimeType: string) => {
|
||||
this.#uploadImageReducer({ data, mimeType });
|
||||
uploadImage = async (data: Uint8Array, mimeType: string, name?: string) => {
|
||||
this.#uploadImageReducer({ data, mimeType, name });
|
||||
};
|
||||
|
||||
toggleReaction = (messageId: bigint, emoji: string) => {
|
||||
|
||||
+3
-2
@@ -18,7 +18,7 @@ export const formatTime = (ts: any) => {
|
||||
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) => {
|
||||
const img = new Image();
|
||||
const reader = new FileReader();
|
||||
@@ -60,7 +60,8 @@ export const optimizeImage = async (file: File): Promise<{ data: Uint8Array, mim
|
||||
const buffer = await blob.arrayBuffer();
|
||||
resolve({
|
||||
data: new Uint8Array(buffer),
|
||||
mimeType: "image/webp"
|
||||
mimeType: "image/webp",
|
||||
name: file.name
|
||||
});
|
||||
}, "image/webp", 0.8);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user