feat(web): зум клавишами + и - в просмотре фото

В режиме просмотра фотографии (обычный и полноэкранный) клавиши + и - меняют
масштаб изображения на шаг, сохраняя точку в центре просмотра. Пока открыт
просмотр, эти клавиши больше не меняют размер миниатюр в сетке.
This commit is contained in:
iu
2026-10-20 22:01:41 +03:00
parent 50b9983be4
commit 35f84399bc
4 changed files with 88 additions and 6 deletions
+70 -3
View File
@@ -8,6 +8,7 @@
import { contextMenu, type ContextMenuItem } from '$lib/stores/context-menu.svelte';
import { transfer } from '$lib/stores/transfer.svelte';
import { viewerActions } from '$lib/stores/viewer-actions.svelte';
import { viewer } from '$lib/stores/viewer.svelte';
import type { Photo } from '$lib/types';
interface Props {
@@ -25,6 +26,8 @@
const current = $derived(photos[index]);
const MAX_ZOOM = 2;
// Шаг зума клавишами `+`/`-`.
const ZOOM_STEP = 1.25;
let viewerEl: HTMLDivElement | undefined = $state();
let scroller: HTMLDivElement | undefined = $state();
@@ -66,6 +69,14 @@
fresh?.preview_url ?? fresh?.thumb_url ?? current?.preview_url ?? current?.thumb_url ?? ''
);
$effect(() => {
// Пока открыт просмотр, клавиши `+`/`-` относятся к зуму фото.
viewer.open = true;
return () => {
viewer.open = false;
};
});
$effect(() => {
const id = current?.id;
fresh = null;
@@ -172,6 +183,52 @@
function prev() {
if (index > 0) onIndexChange(index - 1);
}
/** Масштаб, при котором фото целиком помещается в просмотр. */
function fitScale(): number {
if (!scroller) return 1;
const natW = baseWidth();
const natH = baseHeight();
if (!natW || !natH) return 1;
return Math.min(scroller.clientWidth / natW, scroller.clientHeight / natH, 1);
}
/**
* Зум клавишами: `+`/`-`. Точка в центре просмотра остаётся на месте,
* как при зуме колесом мыши.
*/
function zoomBy(factor: number) {
const el = scroller;
if (!el || !imgEl) return;
const natW = baseWidth();
const natH = baseHeight();
if (!natW || !natH) return;
const fit = fitScale();
const cur = zoomed ? scale : fit;
const next = Math.min(MAX_ZOOM, Math.max(fit, cur * factor));
if (Math.abs(next - cur) < 1e-4) return;
const relX = imgEl.offsetWidth
? (el.scrollLeft + el.clientWidth / 2 - imgEl.offsetLeft) / imgEl.offsetWidth
: 0.5;
const relY = imgEl.offsetHeight
? (el.scrollTop + el.clientHeight / 2 - imgEl.offsetTop) / imgEl.offsetHeight
: 0.5;
zoomed = next > fit * 1.001;
scale = next;
tick().then(() => {
const iw = imgEl?.offsetWidth ?? 0;
const ih = imgEl?.offsetHeight ?? 0;
if (!iw || !ih) return;
const offX = Math.max(0, (el.clientWidth - iw) / 2);
const offY = Math.max(0, (el.clientHeight - ih) / 2);
el.scrollLeft = offX + relX * iw - el.clientWidth / 2;
el.scrollTop = offY + relY * ih - el.clientHeight / 2;
});
}
function next() {
if (index < photos.length - 1) onIndexChange(index + 1);
}
@@ -205,9 +262,9 @@
if (!natW || !natH) return;
const factor = ev.deltaY < 0 ? 1.15 : 1 / 1.15;
const fitScale = Math.min(scroller.clientWidth / natW, scroller.clientHeight / natH, 1);
const curScale = zoomed ? scale : fitScale;
const newScale = Math.min(MAX_ZOOM, Math.max(fitScale, curScale * factor));
const fit = fitScale();
const curScale = zoomed ? scale : fit;
const newScale = Math.min(MAX_ZOOM, Math.max(fit, curScale * factor));
if (Math.abs(newScale - curScale) < 1e-4) return;
const r = imgEl.getBoundingClientRect();
@@ -292,6 +349,16 @@
toggleViewerFullscreen();
return;
}
if (ev.key === '+' || ev.key === '=') {
ev.preventDefault();
zoomBy(ZOOM_STEP);
return;
}
if (ev.key === '-' || ev.key === '_') {
ev.preventDefault();
zoomBy(1 / ZOOM_STEP);
return;
}
if (zoomed) return;
if (ev.key === 'ArrowLeft') prev();
else if (ev.key === 'ArrowRight') next();