feat(web): сохранять размер миниатюр при скрытых боковых панелях
В скрытом режиме панели больше не растягивают плитки: сетка пересчитывает число колонок так, чтобы размер миниатюры остался прежним, а в строке помещалось больше фото. Каркас публикует ширину, которую занимают панели в текущей раскладке (рейл на широких экранах, панель на сверхшироких), сетка использует её для пересчёта, а навигация курсора стрелками учитывает реальное число колонок.
This commit is contained in:
@@ -27,17 +27,32 @@
|
|||||||
|
|
||||||
const RAIL = { min: 176, max: 480, key: 'rail-width', def: 240 };
|
const RAIL = { min: 176, max: 480, key: 'rail-width', def: 240 };
|
||||||
const PANEL = { min: 220, max: 560, key: 'panel-width', def: 288 };
|
const PANEL = { min: 220, max: 560, key: 'panel-width', def: 288 };
|
||||||
|
// Ширина разделителя (w-1) между панелью и содержимым.
|
||||||
|
const HANDLE_PX = 4;
|
||||||
|
|
||||||
let railWidth = $state(RAIL.def);
|
let railWidth = $state(RAIL.def);
|
||||||
let panelWidth = $state(PANEL.def);
|
let panelWidth = $state(PANEL.def);
|
||||||
|
|
||||||
let isFullscreen = $state(false);
|
let isFullscreen = $state(false);
|
||||||
|
let isWide = $state(false);
|
||||||
|
let isExtraWide = $state(false);
|
||||||
|
|
||||||
|
// Ширина, которую занимают панели в текущей раскладке: рейл и панель
|
||||||
|
// встают рядом с содержимым на широких экранах, на узких — поверх него.
|
||||||
|
const reserved = $derived(
|
||||||
|
(isWide ? railWidth + HANDLE_PX : 0) +
|
||||||
|
(isExtraWide && panel && panelVisible ? panelWidth + HANDLE_PX : 0)
|
||||||
|
);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
// Режим скрытия действует только там, где он доступен (просмотр альбома).
|
// Режим скрытия действует только там, где он доступен (просмотр альбома).
|
||||||
if (!panelsToggle) panels.reset();
|
if (!panelsToggle) panels.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
panels.reserved = reserved;
|
||||||
|
});
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const r = parseInt(localStorage.getItem(RAIL.key) ?? '', 10);
|
const r = parseInt(localStorage.getItem(RAIL.key) ?? '', 10);
|
||||||
if (r >= RAIL.min && r <= RAIL.max) railWidth = r;
|
if (r >= RAIL.min && r <= RAIL.max) railWidth = r;
|
||||||
@@ -47,7 +62,22 @@
|
|||||||
const onFs = () => (isFullscreen = !!document.fullscreenElement);
|
const onFs = () => (isFullscreen = !!document.fullscreenElement);
|
||||||
document.addEventListener('fullscreenchange', onFs);
|
document.addEventListener('fullscreenchange', onFs);
|
||||||
onFs();
|
onFs();
|
||||||
return () => document.removeEventListener('fullscreenchange', onFs);
|
|
||||||
|
const mqLg = window.matchMedia('(min-width: 1024px)');
|
||||||
|
const mqXl = window.matchMedia('(min-width: 1280px)');
|
||||||
|
const sync = () => {
|
||||||
|
isWide = mqLg.matches;
|
||||||
|
isExtraWide = mqXl.matches;
|
||||||
|
};
|
||||||
|
sync();
|
||||||
|
mqLg.addEventListener('change', sync);
|
||||||
|
mqXl.addEventListener('change', sync);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('fullscreenchange', onFs);
|
||||||
|
mqLg.removeEventListener('change', sync);
|
||||||
|
mqXl.removeEventListener('change', sync);
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
async function toggleFullscreen() {
|
async function toggleFullscreen() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from './Icon.svelte';
|
import Icon from './Icon.svelte';
|
||||||
import { density } from '$lib/stores/density.svelte';
|
import { grid } from '$lib/stores/grid.svelte';
|
||||||
import { isPanoTag } from '$lib/pano';
|
import { isPanoTag } from '$lib/pano';
|
||||||
import type { Photo } from '$lib/types';
|
import type { Photo } from '$lib/types';
|
||||||
|
|
||||||
@@ -32,6 +32,10 @@
|
|||||||
const GAP_PX = 8;
|
const GAP_PX = 8;
|
||||||
let gridWidth = $state(0);
|
let gridWidth = $state(0);
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
grid.width = gridWidth;
|
||||||
|
});
|
||||||
|
|
||||||
// Offer every variant the photo actually has; the backend generates the
|
// Offer every variant the photo actually has; the backend generates the
|
||||||
// medium variant asynchronously, so an incomplete set must still render.
|
// medium variant asynchronously, so an incomplete set must still render.
|
||||||
function srcsetFor(photo: Photo): string {
|
function srcsetFor(photo: Photo): string {
|
||||||
@@ -44,7 +48,7 @@
|
|||||||
|
|
||||||
const sizes = $derived(
|
const sizes = $derived(
|
||||||
gridWidth > 0
|
gridWidth > 0
|
||||||
? `${Math.max(1, Math.round((gridWidth - (density.cols - 1) * GAP_PX) / density.cols))}px`
|
? `${Math.max(1, Math.round((gridWidth - (grid.cols - 1) * GAP_PX) / grid.cols))}px`
|
||||||
: undefined
|
: undefined
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -76,7 +80,7 @@
|
|||||||
<ul
|
<ul
|
||||||
class="grid gap-2"
|
class="grid gap-2"
|
||||||
bind:clientWidth={gridWidth}
|
bind:clientWidth={gridWidth}
|
||||||
style="grid-template-columns: repeat({density.cols}, minmax(0, 1fr));"
|
style="grid-template-columns: repeat({grid.cols}, minmax(0, 1fr));"
|
||||||
>
|
>
|
||||||
{#each photos as photo, i (photo.id)}
|
{#each photos as photo, i (photo.id)}
|
||||||
<li>
|
<li>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { density } from './density.svelte';
|
||||||
|
import { panels } from './panels.svelte';
|
||||||
|
|
||||||
|
class GridStore {
|
||||||
|
/** Ширина контейнера сетки (px). */
|
||||||
|
width = $state(0);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Число колонок сетки. Когда боковые панели скрыты, сетка шире, поэтому
|
||||||
|
* колонок становится больше, а плитки остаются того же размера, что и с
|
||||||
|
* видимыми панелями.
|
||||||
|
*/
|
||||||
|
cols = $derived.by(() => {
|
||||||
|
const visible = this.width - panels.reserved;
|
||||||
|
if (!panels.hidden || this.width <= 0 || visible <= 0) return density.cols;
|
||||||
|
return Math.max(1, Math.round((density.cols * this.width) / visible));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const grid = new GridStore();
|
||||||
@@ -5,6 +5,8 @@ class PanelsStore {
|
|||||||
railPeek = $state(false);
|
railPeek = $state(false);
|
||||||
/** Правая панель показана у кромки в скрытом режиме. */
|
/** Правая панель показана у кромки в скрытом режиме. */
|
||||||
panelPeek = $state(false);
|
panelPeek = $state(false);
|
||||||
|
/** Ширина (px), которую панели занимают в раскладке, — для пересчёта колонок сетки. */
|
||||||
|
reserved = $state(0);
|
||||||
|
|
||||||
toggle() {
|
toggle() {
|
||||||
this.hidden = !this.hidden;
|
this.hidden = !this.hidden;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
import { auth } from '$lib/stores/auth.svelte';
|
import { auth } from '$lib/stores/auth.svelte';
|
||||||
import { albums } from '$lib/stores/albums.svelte';
|
import { albums } from '$lib/stores/albums.svelte';
|
||||||
import { albumAccessLost } from '$lib/albums-events';
|
import { albumAccessLost } from '$lib/albums-events';
|
||||||
import { density } from '$lib/stores/density.svelte';
|
import { grid } from '$lib/stores/grid.svelte';
|
||||||
import { selection } from '$lib/stores/selection.svelte';
|
import { selection } from '$lib/stores/selection.svelte';
|
||||||
import { photoUpdates } from '$lib/stores/photo-updates.svelte';
|
import { photoUpdates } from '$lib/stores/photo-updates.svelte';
|
||||||
import { trash } from '$lib/stores/trash.svelte';
|
import { trash } from '$lib/stores/trash.svelte';
|
||||||
@@ -408,7 +408,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const origin = cursorIndex;
|
const origin = cursorIndex;
|
||||||
const cols = Math.max(1, density.cols);
|
const cols = Math.max(1, grid.cols);
|
||||||
let next = origin;
|
let next = origin;
|
||||||
if (key === 'ArrowLeft') next = origin - 1;
|
if (key === 'ArrowLeft') next = origin - 1;
|
||||||
else if (key === 'ArrowRight') next = origin + 1;
|
else if (key === 'ArrowRight') next = origin + 1;
|
||||||
|
|||||||
@@ -190,12 +190,18 @@ SHALL выдвигать правую панель поверх содержим
|
|||||||
панели SHALL не занимать место в раскладке, содержимое альбома SHALL занимать всю ширину,
|
панели SHALL не занимать место в раскладке, содержимое альбома SHALL занимать всю ширину,
|
||||||
а панели SHALL показываться при наведении указателя на левую или правую кромку окна и
|
а панели SHALL показываться при наведении указателя на левую или правую кромку окна и
|
||||||
скрываться после ухода указателя. Хоткей SHALL не срабатывать при вводе текста в поля и при
|
скрываться после ухода указателя. Хоткей SHALL не срабатывать при вводе текста в поля и при
|
||||||
открытых модальных диалогах или меню.
|
открытых модальных диалогах или меню. Размер миниатюр в скрытом режиме SHALL сохраняться:
|
||||||
|
вместо растянутых плиток сетка SHALL показывать больше фото в строке.
|
||||||
|
|
||||||
#### Scenario: Переключение кнопкой
|
#### Scenario: Переключение кнопкой
|
||||||
- **WHEN** пользователь нажимает кнопку скрытия боковых панелей в альбоме
|
- **WHEN** пользователь нажимает кнопку скрытия боковых панелей в альбоме
|
||||||
- **THEN** обе панели уезжают за края, а сетка фото занимает всю ширину окна
|
- **THEN** обе панели уезжают за края, а сетка фото занимает всю ширину окна
|
||||||
|
|
||||||
|
#### Scenario: Размер миниатюр при скрытых панелях
|
||||||
|
- **WHEN** боковые панели скрыты
|
||||||
|
- **THEN** плитки остаются того же размера, что и с видимыми панелями, а в строке помещается
|
||||||
|
больше фото
|
||||||
|
|
||||||
#### Scenario: Переключение хоткеем
|
#### Scenario: Переключение хоткеем
|
||||||
- **WHEN** пользователь нажимает `Tab` в режиме просмотра альбома
|
- **WHEN** пользователь нажимает `Tab` в режиме просмотра альбома
|
||||||
- **THEN** боковые панели скрываются или показываются
|
- **THEN** боковые панели скрываются или показываются
|
||||||
|
|||||||
Reference in New Issue
Block a user