fix imageviewer
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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)`);
|
||||
|
||||
Reference in New Issue
Block a user