Skip to content

fix: Populate photo neighbors and album context when deep linking#4273

Closed
diusazzad wants to merge 2 commits intoLycheeOrg:masterfrom
diusazzad:fix/deep-link-navigation
Closed

fix: Populate photo neighbors and album context when deep linking#4273
diusazzad wants to merge 2 commits intoLycheeOrg:masterfrom
diusazzad:fix/deep-link-navigation

Conversation

@diusazzad
Copy link
Copy Markdown

@diusazzad diusazzad commented Apr 9, 2026

This Pull Request addresses the issue where deep-linking directly to a photo (e.g., gallery/<photo_id>) results in a lack of navigation context (Next/Previous photo) and a null album ID on the frontend.

Previously, the backend's PhotoController@get method hardcoded album_id as null and did not provide information about surrounding photos. This forced the frontend to manually manage state, which failed for non-initial page photos and direct entry points.

Changes:
Backend Calculation of Neighbors: Introduced a new action app/Actions/Photo/GetNeighbors.php that calculates the previous and next photo IDs within a given album's context, respecting its specific sorting settings (column and order).
PhotoResource Update: Modified the PhotoResource constructor to accept and return next_photo_id and previous_photo_id.
Controller Logic: Updated PhotoController@get to identify a suitable album for the photo (if it belongs to any) and use the GetNeighbors action to populate the navigation context.
This ensures that direct links have fully functional navigation buttons from the moment they are loaded.

Related Issues
Fixes #4260 (indirectly, as part of the discussion in #4272).

Summary by CodeRabbit

Release Notes

  • New Features

    • Introduced API endpoint to fetch individual photos, including information about neighboring photos for seamless album navigation
    • Improved photo loading mechanism with enhanced asynchronous synchronization when switching between photos in albums
  • Bug Fixes

    • Resolved exiftool detection compatibility issues on Windows systems

@diusazzad diusazzad requested a review from a team as a code owner April 9, 2026 17:14
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

This PR introduces a new API endpoint for fetching individual photos with neighbor (previous/next) information. It adds a GetNeighbors action to compute neighboring photos within an album, implements the corresponding controller endpoint with request validation, updates the photo resource to include neighbor IDs, makes the frontend photo store loading asynchronous, and refines exiftool detection for cross-platform compatibility.

Changes

Cohort / File(s) Summary
Photo Neighbor Lookup
app/Actions/Photo/GetNeighbors.php
New action class that computes previous and next photo IDs within an album by comparing sort column values and breaking ties by ID.
Photo Fetching Endpoint
app/Http/Controllers/Gallery/PhotoController.php, app/Http/Requests/Photo/GetPhotoRequest.php, routes/api_v2.php
New GET /Photo endpoint with dedicated request validation, authorization checks, and neighbor computation via injected GetNeighbors.
Photo Response Structure
app/Http/Resources/Models/PhotoResource.php
Constructor updated to accept optional next_photo_id and previous_photo_id parameters alongside existing album_id and should_downgrade_size_variants.
Frontend Photo Loading
resources/js/stores/PhotoState.ts, resources/js/views/gallery-panels/Album.vue
Store load() action converted to async with fallback to PhotoService.get() when photo not found in collection; component calls updated to await the async operation.
Exiftool Detection
database/migrations/2019_12_25_0600_config_exiftool_ternary.php
Migration updated to use OS-specific commands (where on Windows, command -v on non-Windows) and treat both empty string and null as "not found".

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A hop through photos, neighbor by neighbor,
Finding the next and previous with fervor!
Async awaitings, Windows and Linux too,
Photo fetching made fresh with a query or two! 📸

🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 88.89% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5


ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a0afd2ac-1f00-464a-acfb-a7c6da5b49a6

📥 Commits

Reviewing files that changed from the base of the PR and between 9ec2bfd and bc80994.

📒 Files selected for processing (8)
  • app/Actions/Photo/GetNeighbors.php
  • app/Http/Controllers/Gallery/PhotoController.php
  • app/Http/Requests/Photo/GetPhotoRequest.php
  • app/Http/Resources/Models/PhotoResource.php
  • database/migrations/2019_12_25_0600_config_exiftool_ternary.php
  • resources/js/stores/PhotoState.ts
  • resources/js/views/gallery-panels/Album.vue
  • routes/api_v2.php

public function do(Photo $photo, AbstractAlbum $album): array
{
$sorting = $album->getEffectivePhotoSorting();
$column = $sorting->column->toColumn();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Handle raw-ordering sort columns (e.g., rating_avg) explicitly.

The query uses plain column comparisons/orderBy for all sorts. That bypasses ColumnSortingType::requiresRawOrdering() behavior and can produce invalid/incorrect neighbor selection for raw-expression sorts.

Also applies to: 53-53, 67-73, 78-79

{
$sorting = $album->getEffectivePhotoSorting();
$column = $sorting->column->toColumn();
$order = $sorting->order->toOrder();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Normalize enum order before direction checks.

Line 64 assumes lowercase ('asc'). If sort order is uppercase enum value ('ASC'/'DESC'), neighbor direction logic is wrong for ascending sorts.

Suggested fix
-		$order = $sorting->order->toOrder();
+		$order = strtolower($sorting->order->value);
...
-		$is_asc = ($order === 'asc');
+		$is_asc = ($order === 'asc');

Also applies to: 64-66

Comment on lines +76 to +87
$album = $photo->albums()->first();
$neighbors = ['previous_photo_id' => null, 'next_photo_id' => null];

if ($album !== null) {
$neighbors = $get_neighbors->do($photo, $album);
}

return new PhotoResource(
photo: $photo,
album_id: $album?->id,
should_downgrade_size_variants: !Gate::check(PhotoPolicy::CAN_ACCESS_FULL_PHOTO, [Photo::class, $photo]),
next_photo_id: $neighbors['next_photo_id'],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Use explicit album context instead of first() for multi-album photos.

Line 76 picks an arbitrary attached album. For photos in multiple albums, this can return wrong album_id and wrong neighbors for the viewer’s current route context.

Please resolve album context from request (e.g., optional album_id) and validate membership/access before calling GetNeighbors.

Comment on lines +54 to +57
if (this.photo === undefined) {
const response = await PhotoService.get(this.photoId);
this.photo = response.data;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against stale async responses overwriting the current photo.

On Line 55, PhotoService.get(...) can resolve after photoId has changed; Line 56 then assigns outdated data into this.photo.

Suggested fix
 		if (this.photo === undefined) {
-			const response = await PhotoService.get(this.photoId);
-			this.photo = response.data;
+			const requested_photo_id = this.photoId;
+			const response = await PhotoService.get(requested_photo_id);
+			if (this.photoId !== requested_photo_id) {
+				return;
+			}
+			this.photo = response.data;
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (this.photo === undefined) {
const response = await PhotoService.get(this.photoId);
this.photo = response.data;
}
if (this.photo === undefined) {
const requested_photo_id = this.photoId;
const response = await PhotoService.get(requested_photo_id);
if (this.photoId !== requested_photo_id) {
return;
}
this.photo = response.data;
}

orderManagement.load();
photoStore.photoId = photoId.value;
photoStore.load();
await photoStore.load();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Replace await usage in this Vue component with .then() chaining.

This introduces await in Vue code paths (Lines 228, 243, 498, 518), which conflicts with the project Vue convention.

As per coding guidelines, **/*.vue: “Do not use await async calls in Vue3, use .then() instead”.

Also applies to: 243-243, 498-498, 518-518

@ildyria
Copy link
Copy Markdown
Member

ildyria commented Apr 11, 2026

I already replied on #4272 that this was not the way to do it. You completely ignored my advises and implementation directions. Closing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deep link to photo fails silently when photo is not on first page

2 participants