Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
9776534
feat(settings): add lightbox settings types and default configuration
Darkkal Jul 22, 2026
85a091d
feat(settings): add lightbox section and validation to settings UI
Darkkal Jul 22, 2026
6e88d04
test(settings): add unit and e2e test coverage for lightbox settings
Darkkal Jul 22, 2026
8704a86
fix(settings): prioritize min > max validation order in settings form
Darkkal Jul 22, 2026
9a02f2d
feat(lightbox): replace circular nav buttons with full-height navigat…
Darkkal Jul 22, 2026
b303ba3
test(lightbox): add e2e tests for full-height navigation zones
Darkkal Jul 22, 2026
8cb266d
feat(lightbox): pass auto-hide settings to page client components
Darkkal Jul 22, 2026
1258942
feat(lightbox): implement show/hide controls framework with persisten…
Darkkal Jul 22, 2026
80746bd
test(lightbox): add e2e tests for show/hide controls and auto-hide be…
Darkkal Jul 22, 2026
42027e1
fix(lightbox): reset auto-hide inactivity timer on click, pointer, to…
Darkkal Jul 22, 2026
40407fe
refactor(lightbox): decouple sidebar visibility from auto-hide controls
Darkkal Jul 22, 2026
5395568
feat(lightbox): pass fit mode and zoom bounds settings to page compon…
Darkkal Jul 23, 2026
fe6531f
feat(lightbox): implement fit modes, zoom controls, and mouse/touch d…
Darkkal Jul 23, 2026
6959020
test(lightbox): add e2e tests for lightbox fit mode and zoom controls
Darkkal Jul 23, 2026
30931a1
test(lightbox): update zoom reset assertion in lightbox e2e tests
Darkkal Jul 23, 2026
bb2dedd
fix(lightbox): enable mouse and touch drag panning in fitWidth and fi…
Darkkal Jul 23, 2026
47cb047
test(lightbox): update zoom level regex matchers in e2e tests
Darkkal Jul 23, 2026
17a42a4
fix(lightbox): enforce directional boundaries and prevent controls ov…
Darkkal Jul 23, 2026
ad1b981
feat(lightbox): implement multi-touch pinch-to-zoom with midpoint anc…
Darkkal Jul 23, 2026
92b07be
test(lightbox): add e2e tests for pinch-to-zoom multi-touch interactions
Darkkal Jul 23, 2026
c6253e2
test(lightbox): add touch identifiers to pinch-to-zoom e2e test
Darkkal Jul 23, 2026
70f453d
fix(lightbox): prevent browser-level viewport zoom on mobile touch ge…
Darkkal Jul 23, 2026
76919e4
fix(lightbox): reset zoom to 100% on fit mode change and update defau…
Darkkal Jul 23, 2026
e623f07
docs(lightbox): update architecture and contributing docs for lightbo…
Darkkal Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 24 additions & 1 deletion ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ For day-to-day coding conventions and established patterns, see [CONTRIBUTING.md
- [3.7 Settings & Configuration System](#37-settings--configuration-system)
- [3.8 Task Scheduling Subsystem](#38-task-scheduling-subsystem)
- [3.9 Statistics Subsystem](#39-statistics-subsystem)
- [3.10 Lightbox Media Viewer Subsystem](#310-lightbox-media-viewer-subsystem)
4. [Data Flow](#4-data-flow)
- [4.1 Scrape → Scan → Display](#41-scrape--scan--display)
- [4.2 Client-Side Data Fetching](#42-client-side-data-fetching)
Expand Down Expand Up @@ -119,7 +120,7 @@ web-gallery/
│ │ ├── error.tsx # Root error boundary
│ │ └── not-found.tsx # 404 page
│ ├── components/ # Shared UI components
│ │ ├── Lightbox.tsx # Full-screen media viewer
│ │ ├── Lightbox.tsx # Full-screen media viewer (fit modes, zoom/pan, touch gestures)
│ │ ├── MasonryGrid.tsx # CSS columns masonry layout
│ │ ├── Navbar.tsx # Top navigation bar + theme toggle
│ │ ├── ThemeProvider.tsx # Dark/light theme via data-theme attribute
Expand Down Expand Up @@ -373,6 +374,22 @@ The Statistics Subsystem provides pre-computed counters and historical growth me
- **Proportional Timeline Distribution**: For historical charts, cumulative growth is tracked daily in `statistics_history`. If a metric lacks historical timestamps (such as tags or user entries), the repository maps them proportionally to the daily posts import velocity to provide clean growth curves.
- **REST Endpoints & Client Rendering**: The dashboard fetches its data on-demand from `/api/statistics` for modular caching and instant filters (date granularity, range limits, active page sorting).

### 3.10 Lightbox Media Viewer Subsystem

**Location**: [`src/components/Lightbox.tsx`](./src/components/Lightbox.tsx), [`src/hooks/useLightbox.ts`](./src/hooks/useLightbox.ts)

The Lightbox subsystem provides interactive, full-screen media viewing with configurable zoom, panning, fit modes, navigation, and gesture handling:

- **Fit Mode Cycling**: Supports `fitBoth` (contain within viewport bounds), `fitWidth` (expand to full container width), and `fitHeight` (expand to full container height). Changing fit modes automatically resets zoom level to 100% and pan offset to `{ x: 0, y: 0 }`.
- **Directional Clamping & Pan Boundaries**: Drag panning via mouse or touch is constrained by `clampPanOffset`:
- `fitWidth` mode locks horizontal panning (`x: 0`) and allows vertical panning bounded to keep rendered top/bottom image edges within the viewport.
- `fitHeight` mode locks vertical panning (`y: 0`) and allows horizontal panning bounded to keep rendered left/right image edges within the viewport.
- Zoomed mode (`zoom > 1.0`) allows two-axis panning bounded so image edges can pan up to half the container dimension.
- `fitBoth` at 100% zoom disables panning entirely, preserving swipe-to-navigate.
- **Multi-Touch Pinch-to-Zoom**: Detects two-finger touch events (`onTouchStart`, `onTouchMove`, `onTouchEnd`) to dynamically update zoom level based on touch distance ratios. Uses initial touch midpoint anchoring (`initialPinchCenterRef`) to calculate translation shifts during scaling.
- **Auto-Hide Controls Framework**: Supports a persistent toggle button (`Eye`/`EyeOff`) and configurable inactivity auto-hide timers (`autoHideDelay`). Timer resets on pointer, click, touch, wheel, keyboard, or navigation activity. Pressing `Escape` while controls are hidden restores control visibility before closing on second press.
- **Full-Height Navigation Overlay**: Replaces small target buttons with full-height left and right click/touch zones (`navZonePrev`, `navZoneNext`) for effortless media cycling.

---

## 4. Data Flow
Expand Down Expand Up @@ -584,6 +601,12 @@ Separating fast-access data (database, scraper state) from bulk media allows ope
### FIFO schedule execution for SQLite safety
To prevent multiple concurrent scheduled scraping tasks from locking or corrupting the SQLite database, all scheduled runs are serialized through a sequential FIFO queue. The next scheduled task begins execution only after the current scraper process completes and its database synchronization finishes.

### Scoped browser zoom prevention over global user-scalable=no
Disabling viewport zoom globally via `user-scalable=no` meta tags violates WCAG accessibility standards. To prevent mobile pinch gestures from zooming the entire web application layout while interacting with the media viewer, `Lightbox.tsx` attaches a non-passive `touchmove` event listener (`{ passive: false }`) scoped exclusively to the Lightbox overlay element (`overlayRef`), calling `preventDefault()` only when multi-touch gestures occur.

### Pan offset divided by zoom factor in CSS transform
CSS `transform` matrix operations evaluate transformations right-to-left (`scale()` then `translate()`). In `Lightbox.tsx`, inline styling applies `scale(${zoom}) translate(${panOffset.x / zoom}px, ${panOffset.y / zoom}px)`. Dividing the raw screen-pixel drag offset by the zoom level converts pan deltas back into pre-scaled coordinate space, ensuring 1:1 physical cursor-to-image tracking at any zoom magnification level.

---

## 9. Testing Architecture
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ Frontend types are in `src/types/`:
| `autocomplete.ts` | `AutocompleteSuggestion`, `AutocompleteResponse` definitions |
| `playlist.ts` | `Playlist`, playlist-related types |
| `posts.ts` | `TimelinePost`, post-related types |
| `settings.ts` | `SystemSettings`, `AppSettings` definitions |
| `settings.ts` | `SystemSettings`, `AppSettings` (including Lightbox fit, zoom, and auto-hide config) definitions |
| `source.ts` | `Source`, source-related types |
| `statistics.ts` | `LibraryStatistics`, `StatisticsHistoryPoint`, etc. |
| `users.ts` | `TwitterUser`, `PixivUser` |
Expand Down
36 changes: 36 additions & 0 deletions src/app/gallery/page-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export default function GalleryPageClient({
loopVideos,
autoplayVideos,
muteAutoplayVideos,
autoHideControls,
autoHideDelay,
lightboxFitMode,
lightboxZoomMin,
lightboxZoomMax,
lightboxZoomStep,
playlistId,
}: {
initialItems: GalleryGroup[];
Expand All @@ -41,6 +47,12 @@ export default function GalleryPageClient({
loopVideos: boolean;
autoplayVideos: boolean;
muteAutoplayVideos: boolean;
autoHideControls?: boolean;
autoHideDelay?: number;
lightboxFitMode?: "fitBoth" | "fitWidth" | "fitHeight";
lightboxZoomMin?: number;
lightboxZoomMax?: number;
lightboxZoomStep?: number;
playlistId?: number;
}) {
return (
Expand All @@ -55,6 +67,12 @@ export default function GalleryPageClient({
loopVideos={loopVideos}
autoplayVideos={autoplayVideos}
muteAutoplayVideos={muteAutoplayVideos}
autoHideControls={autoHideControls}
autoHideDelay={autoHideDelay}
lightboxFitMode={lightboxFitMode}
lightboxZoomMin={lightboxZoomMin}
lightboxZoomMax={lightboxZoomMax}
lightboxZoomStep={lightboxZoomStep}
playlistId={playlistId}
/>
);
Expand All @@ -71,6 +89,12 @@ function GalleryPageContent({
loopVideos,
autoplayVideos,
muteAutoplayVideos,
autoHideControls,
autoHideDelay,
lightboxFitMode,
lightboxZoomMin,
lightboxZoomMax,
lightboxZoomStep,
playlistId,
}: {
initialItems: GalleryGroup[];
Expand All @@ -83,6 +107,12 @@ function GalleryPageContent({
loopVideos: boolean;
autoplayVideos: boolean;
muteAutoplayVideos: boolean;
autoHideControls?: boolean;
autoHideDelay?: number;
lightboxFitMode?: "fitBoth" | "fitWidth" | "fitHeight";
lightboxZoomMin?: number;
lightboxZoomMax?: number;
lightboxZoomStep?: number;
playlistId?: number;
}) {
const [columnCount, setColumnCount] = useState(4);
Expand Down Expand Up @@ -369,6 +399,12 @@ function GalleryPageContent({
onRefetch={handleRefetchSingle}
loopVideos={loopVideos}
isPageLoading={isPageLoading}
autoHideControls={autoHideControls}
autoHideDelay={autoHideDelay}
fitMode={lightboxFitMode}
zoomMin={lightboxZoomMin}
zoomMax={lightboxZoomMax}
zoomStep={lightboxZoomStep}
onTagClick={(tagName) => {
setSearchQuery(`tag:${tagName} `);
closeLightbox();
Expand Down
6 changes: 6 additions & 0 deletions src/app/gallery/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ export default async function GalleryPage({
loopVideos={settings.loopVideos}
autoplayVideos={settings.autoplayVideos ?? false}
muteAutoplayVideos={settings.muteAutoplayVideos ?? true}
autoHideControls={settings.lightboxAutoHideControls ?? false}
autoHideDelay={settings.lightboxAutoHideDelay ?? 3}
lightboxFitMode={settings.lightboxFitMode ?? "fitBoth"}
lightboxZoomMin={settings.lightboxZoomMin ?? 50}
lightboxZoomMax={settings.lightboxZoomMax ?? 200}
lightboxZoomStep={settings.lightboxZoomStep ?? 25}
playlistId={
playlistId && !Number.isNaN(playlistId) ? playlistId : undefined
}
Expand Down
Loading
Loading