feat(web): зум клавишами + и - в просмотре фото
В режиме просмотра фотографии (обычный и полноэкранный) клавиши + и - меняют масштаб изображения на шаг, сохраняя точку в центре просмотра. Пока открыт просмотр, эти клавиши больше не меняют размер миниатюр в сетке.
This commit is contained in:
@@ -9,6 +9,7 @@
|
||||
import { auth } from '$lib/stores/auth.svelte';
|
||||
import { density } from '$lib/stores/density.svelte';
|
||||
import { panels } from '$lib/stores/panels.svelte';
|
||||
import { viewer } from '$lib/stores/viewer.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
@@ -178,8 +179,9 @@
|
||||
if (ev.ctrlKey || ev.metaKey || ev.altKey || ev.repeat) return;
|
||||
|
||||
// `+`/`-` меняют размер миниатюр — как кнопки под регулятором.
|
||||
// В просмотре фото эти клавиши отданы зуму.
|
||||
if (ev.key === '+' || ev.key === '=' || ev.key === '-' || ev.key === '_') {
|
||||
if (isTextEntry() || hasOverlay()) return;
|
||||
if (viewer.open || isTextEntry() || hasOverlay()) return;
|
||||
ev.preventDefault();
|
||||
density.stepSize(ev.key === '+' || ev.key === '=' ? 1 : -1);
|
||||
return;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
class ViewerStore {
|
||||
/** Открыт просмотр фото: горячие клавиши отданы зуму, а не сетке. */
|
||||
open = $state(false);
|
||||
}
|
||||
|
||||
export const viewer = new ViewerStore();
|
||||
@@ -83,7 +83,8 @@ SHALL отображаться вне каркаса, но в общей тем
|
||||
сторону уменьшения и увеличения. Выбранный размер SHALL сохраняться между сессиями и SHALL
|
||||
быть общим для обзора альбомов и сетки фото альбома. Клавиши `+` и `-` SHALL менять размер
|
||||
миниатюр на шаг так же, как кнопки регулятора; при вводе текста и в открытых диалогах либо
|
||||
меню эти клавиши SHALL сохранять обычное действие.
|
||||
меню эти клавиши SHALL сохранять обычное действие. В режиме просмотра фото те же клавиши
|
||||
SHALL изменять зум изображения, а не размер миниатюр.
|
||||
|
||||
#### Scenario: Изменение размера
|
||||
|
||||
@@ -254,7 +255,8 @@ SHALL выдвигать правую панель поверх содержим
|
||||
|
||||
### Requirement: Зум в просмотре
|
||||
Просмотр SHALL поддерживать зум колесом мыши без прокрутки, с привязкой к указателю и
|
||||
верхним пределом 200% размеров фото. `Space` SHALL возвращать изображение из любого зума во
|
||||
верхним пределом 200% размеров фото. Клавиши `+` и `-` SHALL менять масштаб на шаг, сохраняя
|
||||
точку в центре просмотра. `Space` SHALL возвращать изображение из любого зума во
|
||||
вписанный вид, а из вписанного — в 100%. При зуме SHALL догружаться оригинал изображения.
|
||||
Стрелки соседних фото SHALL появляться у левой/правой кромки и прятаться в зуме. Зум SHALL
|
||||
работать и в полноэкранном режиме.
|
||||
@@ -263,6 +265,11 @@ SHALL выдвигать правую панель поверх содержим
|
||||
- **WHEN** пользователь вращает колесо мыши над фото
|
||||
- **THEN** масштаб меняется относительно курсора, страница не прокручивается, предел — 200%
|
||||
|
||||
#### Scenario: Зум клавишами
|
||||
- **WHEN** пользователь нажимает `+` или `-` в режиме просмотра фото
|
||||
- **THEN** масштаб меняется на шаг, точка в центре просмотра остаётся на месте, а в полноэкранном
|
||||
режиме зум работает так же
|
||||
|
||||
#### Scenario: Пробел
|
||||
- **WHEN** пользователь нажимает `Space`
|
||||
- **THEN** из вписанного вида изображение переходит в 100%, а из любого зума возвращается во вписанный
|
||||
|
||||
Reference in New Issue
Block a user