feat(web): зум клавишами + и - в просмотре фото
В режиме просмотра фотографии (обычный и полноэкранный) клавиши + и - меняют масштаб изображения на шаг, сохраняя точку в центре просмотра. Пока открыт просмотр, эти клавиши больше не меняют размер миниатюр в сетке.
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
import { auth } from '$lib/stores/auth.svelte';
|
import { auth } from '$lib/stores/auth.svelte';
|
||||||
import { density } from '$lib/stores/density.svelte';
|
import { density } from '$lib/stores/density.svelte';
|
||||||
import { panels } from '$lib/stores/panels.svelte';
|
import { panels } from '$lib/stores/panels.svelte';
|
||||||
|
import { viewer } from '$lib/stores/viewer.svelte';
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -178,8 +179,9 @@
|
|||||||
if (ev.ctrlKey || ev.metaKey || ev.altKey || ev.repeat) return;
|
if (ev.ctrlKey || ev.metaKey || ev.altKey || ev.repeat) return;
|
||||||
|
|
||||||
// `+`/`-` меняют размер миниатюр — как кнопки под регулятором.
|
// `+`/`-` меняют размер миниатюр — как кнопки под регулятором.
|
||||||
|
// В просмотре фото эти клавиши отданы зуму.
|
||||||
if (ev.key === '+' || ev.key === '=' || ev.key === '-' || ev.key === '_') {
|
if (ev.key === '+' || ev.key === '=' || ev.key === '-' || ev.key === '_') {
|
||||||
if (isTextEntry() || hasOverlay()) return;
|
if (viewer.open || isTextEntry() || hasOverlay()) return;
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
density.stepSize(ev.key === '+' || ev.key === '=' ? 1 : -1);
|
density.stepSize(ev.key === '+' || ev.key === '=' ? 1 : -1);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import { contextMenu, type ContextMenuItem } from '$lib/stores/context-menu.svelte';
|
import { contextMenu, type ContextMenuItem } from '$lib/stores/context-menu.svelte';
|
||||||
import { transfer } from '$lib/stores/transfer.svelte';
|
import { transfer } from '$lib/stores/transfer.svelte';
|
||||||
import { viewerActions } from '$lib/stores/viewer-actions.svelte';
|
import { viewerActions } from '$lib/stores/viewer-actions.svelte';
|
||||||
|
import { viewer } from '$lib/stores/viewer.svelte';
|
||||||
import type { Photo } from '$lib/types';
|
import type { Photo } from '$lib/types';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -25,6 +26,8 @@
|
|||||||
const current = $derived(photos[index]);
|
const current = $derived(photos[index]);
|
||||||
|
|
||||||
const MAX_ZOOM = 2;
|
const MAX_ZOOM = 2;
|
||||||
|
// Шаг зума клавишами `+`/`-`.
|
||||||
|
const ZOOM_STEP = 1.25;
|
||||||
|
|
||||||
let viewerEl: HTMLDivElement | undefined = $state();
|
let viewerEl: HTMLDivElement | undefined = $state();
|
||||||
let scroller: HTMLDivElement | undefined = $state();
|
let scroller: HTMLDivElement | undefined = $state();
|
||||||
@@ -66,6 +69,14 @@
|
|||||||
fresh?.preview_url ?? fresh?.thumb_url ?? current?.preview_url ?? current?.thumb_url ?? ''
|
fresh?.preview_url ?? fresh?.thumb_url ?? current?.preview_url ?? current?.thumb_url ?? ''
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
// Пока открыт просмотр, клавиши `+`/`-` относятся к зуму фото.
|
||||||
|
viewer.open = true;
|
||||||
|
return () => {
|
||||||
|
viewer.open = false;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const id = current?.id;
|
const id = current?.id;
|
||||||
fresh = null;
|
fresh = null;
|
||||||
@@ -172,6 +183,52 @@
|
|||||||
function prev() {
|
function prev() {
|
||||||
if (index > 0) onIndexChange(index - 1);
|
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() {
|
function next() {
|
||||||
if (index < photos.length - 1) onIndexChange(index + 1);
|
if (index < photos.length - 1) onIndexChange(index + 1);
|
||||||
}
|
}
|
||||||
@@ -205,9 +262,9 @@
|
|||||||
if (!natW || !natH) return;
|
if (!natW || !natH) return;
|
||||||
|
|
||||||
const factor = ev.deltaY < 0 ? 1.15 : 1 / 1.15;
|
const factor = ev.deltaY < 0 ? 1.15 : 1 / 1.15;
|
||||||
const fitScale = Math.min(scroller.clientWidth / natW, scroller.clientHeight / natH, 1);
|
const fit = fitScale();
|
||||||
const curScale = zoomed ? scale : fitScale;
|
const curScale = zoomed ? scale : fit;
|
||||||
const newScale = Math.min(MAX_ZOOM, Math.max(fitScale, curScale * factor));
|
const newScale = Math.min(MAX_ZOOM, Math.max(fit, curScale * factor));
|
||||||
if (Math.abs(newScale - curScale) < 1e-4) return;
|
if (Math.abs(newScale - curScale) < 1e-4) return;
|
||||||
|
|
||||||
const r = imgEl.getBoundingClientRect();
|
const r = imgEl.getBoundingClientRect();
|
||||||
@@ -292,6 +349,16 @@
|
|||||||
toggleViewerFullscreen();
|
toggleViewerFullscreen();
|
||||||
return;
|
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 (zoomed) return;
|
||||||
if (ev.key === 'ArrowLeft') prev();
|
if (ev.key === 'ArrowLeft') prev();
|
||||||
else if (ev.key === 'ArrowRight') next();
|
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 менять размер
|
||||||
миниатюр на шаг так же, как кнопки регулятора; при вводе текста и в открытых диалогах либо
|
миниатюр на шаг так же, как кнопки регулятора; при вводе текста и в открытых диалогах либо
|
||||||
меню эти клавиши SHALL сохранять обычное действие.
|
меню эти клавиши SHALL сохранять обычное действие. В режиме просмотра фото те же клавиши
|
||||||
|
SHALL изменять зум изображения, а не размер миниатюр.
|
||||||
|
|
||||||
#### Scenario: Изменение размера
|
#### Scenario: Изменение размера
|
||||||
|
|
||||||
@@ -254,7 +255,8 @@ SHALL выдвигать правую панель поверх содержим
|
|||||||
|
|
||||||
### Requirement: Зум в просмотре
|
### Requirement: Зум в просмотре
|
||||||
Просмотр SHALL поддерживать зум колесом мыши без прокрутки, с привязкой к указателю и
|
Просмотр SHALL поддерживать зум колесом мыши без прокрутки, с привязкой к указателю и
|
||||||
верхним пределом 200% размеров фото. `Space` SHALL возвращать изображение из любого зума во
|
верхним пределом 200% размеров фото. Клавиши `+` и `-` SHALL менять масштаб на шаг, сохраняя
|
||||||
|
точку в центре просмотра. `Space` SHALL возвращать изображение из любого зума во
|
||||||
вписанный вид, а из вписанного — в 100%. При зуме SHALL догружаться оригинал изображения.
|
вписанный вид, а из вписанного — в 100%. При зуме SHALL догружаться оригинал изображения.
|
||||||
Стрелки соседних фото SHALL появляться у левой/правой кромки и прятаться в зуме. Зум SHALL
|
Стрелки соседних фото SHALL появляться у левой/правой кромки и прятаться в зуме. Зум SHALL
|
||||||
работать и в полноэкранном режиме.
|
работать и в полноэкранном режиме.
|
||||||
@@ -263,6 +265,11 @@ SHALL выдвигать правую панель поверх содержим
|
|||||||
- **WHEN** пользователь вращает колесо мыши над фото
|
- **WHEN** пользователь вращает колесо мыши над фото
|
||||||
- **THEN** масштаб меняется относительно курсора, страница не прокручивается, предел — 200%
|
- **THEN** масштаб меняется относительно курсора, страница не прокручивается, предел — 200%
|
||||||
|
|
||||||
|
#### Scenario: Зум клавишами
|
||||||
|
- **WHEN** пользователь нажимает `+` или `-` в режиме просмотра фото
|
||||||
|
- **THEN** масштаб меняется на шаг, точка в центре просмотра остаётся на месте, а в полноэкранном
|
||||||
|
режиме зум работает так же
|
||||||
|
|
||||||
#### Scenario: Пробел
|
#### Scenario: Пробел
|
||||||
- **WHEN** пользователь нажимает `Space`
|
- **WHEN** пользователь нажимает `Space`
|
||||||
- **THEN** из вписанного вида изображение переходит в 100%, а из любого зума возвращается во вписанный
|
- **THEN** из вписанного вида изображение переходит в 100%, а из любого зума возвращается во вписанный
|
||||||
|
|||||||
Reference in New Issue
Block a user