Skip to content
Open
Changes from all commits
Commits
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
88 changes: 62 additions & 26 deletions immichFrame.Web/src/lib/components/home-page/home-page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@

async function loadAssets() {
try {
let assetRequest = await api.getAsset();
let assetRequest = await api.getAsset({ clientIdentifier: $clientIdentifierStore });

if (assetRequest.status != 200) {
if (assetRequest.status == 401) {
Expand Down Expand Up @@ -154,18 +154,10 @@
return;
}

let next: api.AssetResponseDto[];
if (
$configStore.layout?.trim().toLowerCase() == 'splitview' &&
assetBacklog.length > 1 &&
isHorizontal(assetBacklog[0]) &&
isHorizontal(assetBacklog[1])
) {
next = assetBacklog.splice(0, 2);
} else {
next = assetBacklog.splice(0, 1);
}
assetBacklog = [...assetBacklog];
const candidates = assetBacklog.slice(0, 3);
const selectedIndices = selectAssetsForDisplay($configStore.layout, candidates);
const next = selectedIndices.map((i) => assetBacklog[i]);
assetBacklog = removeAtIndices(assetBacklog, selectedIndices);

if (displayingAssets) {
// Push to History
Expand All @@ -187,19 +179,15 @@
return;
}

let next: api.AssetResponseDto[];
if (
$configStore.layout?.trim().toLowerCase() == 'splitview' &&
assetHistory.length > 1 &&
isHorizontal(assetHistory[assetHistory.length - 1]) &&
isHorizontal(assetHistory[assetHistory.length - 2])
) {
next = assetHistory.splice(assetHistory.length - 2, 2);
} else {
next = assetHistory.splice(assetHistory.length - 1, 1);
}
// get up to 3 candidates from the end of history, reversed for selection logic
const historyLength = assetHistory.length;
const candidates = assetHistory.slice(historyLength - Math.min(3, historyLength)).reverse();
const selectedIndices = selectAssetsForDisplay($configStore.layout, candidates);

assetHistory = [...assetHistory];
// convert indices back to history positions and reverse to restore original display order
const historyIndices = selectedIndices.map((i) => historyLength - 1 - i).reverse();
const next = historyIndices.map((i) => assetHistory[i]);
assetHistory = removeAtIndices(assetHistory, historyIndices);

// Unshift to Backlog
if (displayingAssets) {
Expand All @@ -210,7 +198,7 @@
imagesState = await loadImages(next);
}

function isHorizontal(asset: api.AssetResponseDto) {
function isPortrait(asset: api.AssetResponseDto) {
const isFlipped = (orientation: number) => [5, 6, 7, 8].includes(orientation);
let imageHeight = asset.exifInfo?.exifImageHeight ?? 0;
let imageWidth = asset.exifInfo?.exifImageWidth ?? 0;
Expand All @@ -220,6 +208,54 @@
return imageHeight > imageWidth; // or imageHeight > imageWidth * 1.25;
}

function removeAtIndices<T>(arr: T[], indicesToRemove: number[]): T[] {
const skipSet = new Set(indicesToRemove);
return arr.filter((_, i) => !skipSet.has(i));
}

// Selects which assets to display based on layout and orientation
// For splitview, tries to pair portrait images together, to be shown side by side
// Landscape images are shown alone
function selectAssetsForDisplay(layout: string | undefined, candidates: api.AssetResponseDto[]): number[] {
if (candidates.length < 1) {
return [];
}
if (candidates.length === 1 || layout?.trim().toLowerCase() !== 'splitview') {
return [0];
}

const isImage0Portrait = isPortrait(candidates[0]);
const isImage1Portrait = candidates.length > 1 ? isPortrait(candidates[1]) : false;
const isImage2Portrait = candidates.length > 2 ? isPortrait(candidates[2]) : false;

if (!isImage0Portrait) {
// first image is landscape, show it alone
return [0];
}

// otherwise, first image is portrait

if (candidates.length > 1 && isImage1Portrait) {
// pair with second if it's also portrait
return [0, 1];
}


if (candidates.length > 2 && isImage2Portrait) {
// pair with third if it's portrait (skip landscape second)
return [0, 2];
}


if (candidates[1] != null) {
// no portrait pair found, show second (landscape) instead
return [1];
}
Comment on lines +249 to +253
Copy link

@coderabbitai coderabbitai bot Jan 19, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Portrait image at index 0 can get stuck indefinitely in the backlog.

When the first candidate is portrait and no other portrait exists to pair with (lines 250-252), returning [1] removes only the landscape at index 1. The portrait at index 0 remains in the backlog. On subsequent iterations, if the backlog continues to have only landscapes after this portrait, the same portrait will repeatedly stay stuck while landscapes are consumed.

Consider falling back to displaying the lone portrait instead of skipping it:

🐛 Proposed fix
-	if (candidates[1] != null) {
-		// no portrait pair found, show second (landscape) instead
-		return [1];
-	}
-
-	// Unreachable state, but fallback to returning the first image.
-	return [0];
+	// no portrait pair found, show the portrait alone
+	return [0];

Alternatively, if the intent is to prioritize landscapes over lone portraits, consider a different backlog management strategy that doesn't leave portraits stuck.

🤖 Prompt for AI Agents
In `@immichFrame.Web/src/lib/components/home-page/home-page.svelte` around lines
249 - 253, The current branch that handles portrait/landscape pairing returns
[1] (show the landscape) when candidates[0] is a portrait and no other portrait
exists, which leaves the portrait at index 0 stuck in the backlog; update that
branch in home-page.svelte (the block referencing candidates and the line with
"return [1]") to instead fall back to displaying the lone portrait (return [0])
so the portrait gets shown rather than indefinitely remaining in the backlog.
Ensure you only change the logic in that specific conditional handling of
candidates so other pairing behavior is unchanged.

Copy link
Author

@Zaida-3dO Zaida-3dO Jan 31, 2026

Choose a reason for hiding this comment

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

I think this is acceptable, if you have splitview set to true.

Maybe i just update the docs to explain this caveat, will wait for human input before I make any change here

Copy link

Choose a reason for hiding this comment

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

@Zaida-3dO, understood! That's a reasonable approach—documenting the behavior keeps users informed about how splitview handles mixed portrait/landscape content. If you'd like any help drafting the documentation for this caveat, feel free to ask. Otherwise, I'll defer to the human feedback you're waiting for. 👍


// Unreachable state, but fallback to returning the first image.
return [0];
}

function hasBirthday(assets: api.AssetResponseDto[]) {
let today = new Date();
let hasBday: boolean = false;
Expand Down
Loading