fix imageviewer

This commit is contained in:
2026-04-17 03:42:51 -04:00
parent fbdc19e8ab
commit ccea3cc9a5
2 changed files with 56 additions and 66 deletions
+54 -66
View File
@@ -1,14 +1,19 @@
<script lang="ts">
import { getContext } from "svelte";
import type { ChatService } from "../services/chat.svelte";
import type * as Types from "../../module_bindings/types";
let { image, onClose }: { image: Types.Image, onClose: () => void } = $props();
const chat = getContext<ChatService>("chat");
const imageUrl = $derived(chat.getImageUrl(image.id));
let imgRef = $state<HTMLImageElement | null>(null);
let baseScale = $state(1.0); // Scale required to fit image in shadowbox (90% viewport)
let zoomLevel = $state(1.0); // Absolute scale (e.g. 0.1 to 3.0)
let baseScale = $state(1.0);
let zoomLevel = $state(1.0);
let isDragging = $state(false);
let isChangingZoom = $state(false); // New flag to enable animation during programmatic jumps
let isChangingZoom = $state(false);
let startX = 0;
let startY = 0;
let translateX = $state(0);
@@ -21,9 +26,7 @@
const isZoomed = $derived(zoomLevel > baseScale + 0.001);
function handleKeydown(e: KeyboardEvent) {
if (e.key === "Escape") {
onClose();
}
if (e.key === "Escape") onClose();
}
function onImageLoad(e: Event) {
@@ -33,9 +36,7 @@
const viewportW = window.innerWidth * 0.9;
const viewportH = window.innerHeight * 0.9;
// How much we scale the natural image to fit the 90% shadowbox
baseScale = Math.min(1.0, viewportW / naturalW, viewportH / naturalH);
// Initial zoom is "fitted"
zoomLevel = baseScale;
}
@@ -44,7 +45,6 @@
if (isZoomed) {
resetZoom();
} else {
// Zoom to 2x fit size
zoomTo(Math.max(1.0, baseScale * 2.0));
}
}
@@ -52,10 +52,8 @@
function resetZoom() {
isChangingZoom = true;
zoomLevel = baseScale;
translateX = 0;
translateY = 0;
lastTranslateX = 0;
lastTranslateY = 0;
translateX = 0; translateY = 0;
lastTranslateX = 0; lastTranslateY = 0;
setTimeout(() => { isChangingZoom = false; }, 150);
}
@@ -67,7 +65,6 @@
function handleImageClick(e: MouseEvent) {
e.stopPropagation();
const clickDuration = Date.now() - mousedownTime;
if (hasMoved || clickDuration > 300) return;
@@ -76,10 +73,8 @@
} else {
const target = e.currentTarget as HTMLImageElement;
const rect = target.getBoundingClientRect();
const clickX = e.clientX - rect.left;
const clickY = e.clientY - rect.top;
const w = target.clientWidth;
const h = target.clientHeight;
@@ -88,10 +83,8 @@
isChangingZoom = true;
zoomLevel = targetScale;
translateX = (w / 2 - clickX) * (S - 1);
translateY = (h / 2 - clickY) * (S - 1);
lastTranslateX = translateX;
lastTranslateY = translateY;
setTimeout(() => { isChangingZoom = false; }, 150);
@@ -112,14 +105,9 @@
function handleMouseMove(e: MouseEvent) {
if (!isDragging) return;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) {
hasMoved = true;
}
if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) hasMoved = true;
translateX = lastTranslateX + deltaX;
translateY = lastTranslateY + deltaY;
}
@@ -136,18 +124,17 @@
function handleDownload(e: MouseEvent) {
e.stopPropagation();
const blob = new Blob([image.data], { type: image.mimeType });
const url = URL.createObjectURL(blob);
if (!imageUrl) return;
const a = document.createElement("a");
a.href = url;
a.href = imageUrl;
a.download = image.name || "download.png";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
const formatSize = (bytes: number) => {
const formatSize = (bytes: number | undefined) => {
if (!bytes) return "Unknown size";
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
@@ -167,8 +154,6 @@
onclick={handleOverlayClick}
onmousedown={handleMouseDown}
>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="image-viewer-info"
onclick={(e) => e.stopPropagation()}
@@ -176,32 +161,20 @@
>
<div class="info-filename">{image.name || "Untitled Image"}</div>
<div class="info-details">
{image.mimeType}{formatSize(image.data.length)}{Math.round(zoomLevel * 100)}%
{image.mimeType}{formatSize(chat.imageSizes.get(image.id.toString()))}{Math.round(zoomLevel * 100)}%
</div>
</div>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="image-viewer-actions"
onclick={(e) => e.stopPropagation()}
onmousedown={(e) => e.stopPropagation()}
>
<button
class="action-btn"
onclick={handleDownload}
title="Save Image"
aria-label="Save image"
>
<button class="action-btn" onclick={handleDownload} title="Save Image">
<i class="fas fa-download"></i>
</button>
<div class="zoom-control-container">
<button
class="action-btn"
onclick={toggleZoom}
title={isZoomed ? "Reset Zoom" : "Zoom In"}
aria-label={isZoomed ? "Reset zoom" : "Zoom in"}
>
<button class="action-btn" onclick={toggleZoom} title={isZoomed ? "Reset Zoom" : "Zoom In"}>
<i class="fas {isZoomed ? 'fa-search-minus' : 'fa-search-plus'}"></i>
</button>
<div class="zoom-slider-drawer">
@@ -217,35 +190,37 @@
onmouseup={() => { isChangingZoom = false; }}
oninput={() => {
if (zoomLevel <= baseScale + 0.001) {
translateX = 0;
translateY = 0;
translateX = 0; translateY = 0;
}
}}
/>
</div>
</div>
</div>
<button
class="action-btn close"
onclick={(e) => { e.stopPropagation(); onClose(); }}
aria-label="Close image viewer"
>
<button class="action-btn close" onclick={(e) => { e.stopPropagation(); onClose(); }}>
<i class="fas fa-times"></i>
</button>
</div>
<div class="image-viewer-content">
<img
bind:this={imgRef}
src={URL.createObjectURL(new Blob([image.data], { type: image.mimeType }))}
alt="Full resolution"
class="full-image"
class:animate={isChangingZoom}
style="transform: translate({translateX}px, {translateY}px) scale({zoomLevel / baseScale}); cursor: {isZoomed ? (isDragging ? 'grabbing' : 'zoom-out') : 'zoom-in'}"
onclick={handleImageClick}
onload={onImageLoad}
draggable="false"
/>
{#if imageUrl}
<img
bind:this={imgRef}
src={imageUrl}
alt="Full resolution"
class="full-image"
class:animate={isChangingZoom}
style="transform: translate({translateX}px, {translateY}px) scale({zoomLevel / baseScale}); cursor: {isZoomed ? (isDragging ? 'grabbing' : 'zoom-out') : 'zoom-in'}"
onclick={handleImageClick}
onload={onImageLoad}
draggable="false"
/>
{:else}
<div class="loading-state">
<i class="fas fa-circle-notch fa-spin"></i>
<span>Downloading image data...</span>
</div>
{/if}
</div>
</div>
@@ -405,11 +380,11 @@
align-items: center;
justify-content: center;
overflow: hidden;
pointer-events: none; /* Let clicks pass through to overlay */
pointer-events: none;
}
.image-viewer-content img {
pointer-events: auto; /* Re-enable for the image itself */
pointer-events: auto;
}
.full-image {
@@ -430,4 +405,17 @@
.full-image.animate {
transition: transform 0.05s cubic-bezier(0.4, 0, 0.2, 1);
}
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
color: white;
}
.loading-state i {
font-size: 3rem;
color: var(--brand);
}
</style>
+2
View File
@@ -39,6 +39,7 @@ export class ChatService {
#bannerUrls = new SvelteMap<string, string>();
#serverAvatarUrls = new SvelteMap<string, string>();
#messageImageUrls = new SvelteMap<string, string>();
imageSizes = new SvelteMap<string, number>();
constructor(initialIdentity: Identity | null) {
console.log("ChatService: Initializing with identity:", initialIdentity?.toHexString());
@@ -177,6 +178,7 @@ export class ChatService {
if (metadata) {
// Use a copy of the data
const dataCopy = blob.data.slice();
this.imageSizes.set(idStr, dataCopy.length);
const browserBlob = new Blob([dataCopy], { type: metadata.mimeType });
const url = URL.createObjectURL(browserBlob);
console.log(`[ChatService] Lazy-loaded Blob URL for ${idStr}: ${url} (${dataCopy.length} bytes)`);