Repository: LycheeOrg/Lychee (language: php)

Issue:
Back Button in Frame does not allow to come back.

### Discussed in https://github.com/LycheeOrg/Lychee/discussions/3278

<div type='discussions-op-text'>

<sup>Originally posted by **tinohager** April 25, 2025</sup>
### Lychee version

v6.5.2

### Which PHP version are you using?

PHP 8.4

### Detailed description of the problem

Frame Button navigates me into a state from which I can no longer escape.
I logged in as a user and when I pressed the very prominent frame button I was trapped.

![image](https://github.com/user-attachments/assets/500350ab-704c-4f22-9131-2d055a12b78b)

![image](https://github.com/user-attachments/assets/75aae846-01a4-4e60-b651-2b8818c52edc)


### Steps to reproduce the issue

_No response_

### Diagnostics [REQUIRED]

_No response_

### Browser & System [REQUIRED]

_No response_

### Please confirm (incomplete submissions will not be addressed)

- [X] I have provided easy and step-by-step instructions to reproduce the bug.
- [X] I understand my bug report will be removed if I haven't met the criteria above.</div>
Favourites - Click on photo not works

### Discussed in https://github.com/LycheeOrg/Lychee/discussions/3282

<div type='discussions-op-text'>

<sup>Originally posted by **tinohager** April 25, 2025</sup>
### Lychee version

v6.5.2

### Which PHP version are you using?

PHP 8.4

### Detailed description of the problem

After I have added a photo to my favorites and then switched to the favorites overview, I cannot click on a photo.

![image](https://github.com/user-attachments/assets/999fb59c-3b80-41c3-9267-db526b193e35)


### Steps to reproduce the issue

_No response_

### Diagnostics [REQUIRED]

_No response_

### Browser & System [REQUIRED]

_No response_

### Please confirm (incomplete submissions will not be addressed)

- [X] I have provided easy and step-by-step instructions to reproduce the bug.
- [X] I understand my bug report will be removed if I haven't met the criteria above.</div>
Login Button does not work on Changelog Page

### Discussed in https://github.com/LycheeOrg/Lychee/discussions/3277

<div type='discussions-op-text'>

<sup>Originally posted by **tinohager** April 25, 2025</sup>
### Lychee version

v6.5.2

### Which PHP version are you using?

PHP 8.4

### Detailed description of the problem

Login Button not work after i switch to Changelog, there is then no way back..
`https:///lychee.domain.com/changelogs`

### Steps to reproduce the issue

_No response_

### Diagnostics [REQUIRED]

_No response_

### Browser & System [REQUIRED]

_No response_

### Please confirm (incomplete submissions will not be addressed)

- [X] I have provided easy and step-by-step instructions to reproduce the bug.
- [X] I understand my bug report will be removed if I haven't met the criteria above.</div>


Discussion / hints:

Fix patch:
diff --git a/eslint.config.ts b/eslint.config.ts
index 1c7df8c7505..5e672be8a4f 100644
--- a/eslint.config.ts
+++ b/eslint.config.ts
@@ -25,4 +25,20 @@ export default defineConfigWithVueTs(
     files: ['src/**/__tests__/*'],
   },
   skipFormatting,
+  {
+    "rules": {
+      "@typescript-eslint/no-unused-vars": [
+        "error",
+        {
+          "args": "all",
+          "argsIgnorePattern": "^_",
+          "caughtErrors": "all",
+          "caughtErrorsIgnorePattern": "^_",
+          "destructuredArrayIgnorePattern": "^_",
+          "varsIgnorePattern": "^_",
+          "ignoreRestSiblings": true
+        }
+      ]
+    }
+  }
 )
diff --git a/resources/js/components/gallery/albumModule/AlbumThumbPanel.vue b/resources/js/components/gallery/albumModule/AlbumThumbPanel.vue
index 3a901419ab7..6db9f9c7a09 100644
--- a/resources/js/components/gallery/albumModule/AlbumThumbPanel.vue
+++ b/resources/js/components/gallery/albumModule/AlbumThumbPanel.vue
@@ -1,5 +1,5 @@
 <template>
-	<Panel v-if="props.isTimeline === false" :header="$t(props.header)" :toggleable="!isAlone" :pt:header:class="headerClass" class="border-0 w-full">
+	<Panel v-if="isTimeline === false" :header="$t(props.header)" :toggleable="!isAlone" :pt:header:class="headerClass" class="border-0 w-full">
 		<div class="flex flex-wrap flex-row shrink w-full justify-start gap-1 sm:gap-2 md:gap-4 pt-4">
 			<AlbumThumbPanelList
 				:albums="props.albums"
@@ -32,7 +32,7 @@
 			</template>
 		</Timeline>
 		<div v-else>
-			<template v-for="albumTimeline in albumsTimeLine">
+			<template v-for="(albumTimeline, idx) in albumsTimeLine" :key="'albumTimeline' + idx">
 				<div
 					v-if="albumTimeline.data.filter((a) => !a.is_nsfw || are_nsfw_visible).length > 0"
 					class="flex flex-wrap flex-row shrink w-full justify-start gap-1 sm:gap-2 md:gap-4 pb-8"
@@ -101,6 +101,8 @@ const albumsTimeLine = computed<SplitData<App.Http.Resources.Models.ThumbAlbumRe
 	),
 );
 
+const isTimeline = computed(() => props.isTimeline && albumsTimeLine.value.length > 1);
+
 const headerClass = computed(() => {
 	return props.isAlone ? "hidden" : "";
 });
diff --git a/resources/js/components/gallery/albumModule/PhotoThumbPanel.vue b/resources/js/components/gallery/albumModule/PhotoThumbPanel.vue
index f62cee1a972..cb2bbc06d4d 100644
--- a/resources/js/components/gallery/albumModule/PhotoThumbPanel.vue
+++ b/resources/js/components/gallery/albumModule/PhotoThumbPanel.vue
@@ -11,15 +11,20 @@
 			:galleryConfig="props.galleryConfig"
 			:selectedPhotos="props.selectedPhotos"
 			:iter="0"
+			:idx="0"
 			@clicked="propagateClicked"
 			@selected="propagateSelected"
 			@contexted="propagateMenuOpen"
 			:isTimeline="isTimeline"
 		/>
 		<template v-else>
-			<Timeline v-if="is_timeline_left_border_visible" :value="photosTimeLine" :pt:eventopposite:class="'hidden'" class="mt-4">
+			<Timeline v-if="isLeftBorderVisible" :value="photosTimeLine" :pt:eventopposite:class="'hidden'" class="mt-4">
 				<template #content="slotProps">
-					<div class="flex flex-wrap flex-row shrink w-full justify-start gap-1 sm:gap-2 md:gap-4 pb-8">
+					<div
+						data-type="timelineBlock"
+						:data-date="slotProps.item.data[0].timeline.timeDate"
+						class="flex flex-wrap flex-row shrink w-full justify-start gap-1 sm:gap-2 md:gap-4 pb-8"
+					>
 						<div class="w-full text-left font-semibold text-muted-color-emphasis text-lg">{{ slotProps.item.header }}</div>
 						<PhotoThumbPanelList
 							:photos="slotProps.item.data"
@@ -28,6 +33,7 @@
 							:galleryConfig="props.galleryConfig"
 							:selectedPhotos="props.selectedPhotos"
 							:iter="slotProps.item.iter"
+							:idx="slotProps.index"
 							:isTimeline="isTimeline"
 							@contexted="propagateMenuOpen"
 							@selected="propagateSelected"
@@ -37,8 +43,12 @@
 				</template>
 			</Timeline>
 			<div v-else>
-				<template v-for="photoTimeline in photosTimeLine">
-					<div class="flex flex-wrap flex-row shrink w-full justify-start gap-1 sm:gap-2 md:gap-4 pb-8">
+				<template v-for="(photoTimeline, idx) in photosTimeLine" :key="'photoTimeline' + idx">
+					<div
+						data-type="timelineBlock"
+						:data-date="photoTimeline.data[0].timeline?.time_date"
+						class="flex flex-wrap flex-row shrink w-full justify-start gap-1 sm:gap-2 md:gap-4 pb-8"
+					>
 						<div class="w-full text-left font-semibold text-muted-color-emphasis text-lg">{{ photoTimeline.header }}</div>
 						<PhotoThumbPanelList
 							:photos="photoTimeline.data"
@@ -47,6 +57,7 @@
 							:galleryConfig="props.galleryConfig"
 							:selectedPhotos="props.selectedPhotos"
 							:iter="photoTimeline.iter"
+							:idx="idx"
 							:isTimeline="isTimeline"
 							@contexted="propagateMenuOpen"
 							@selected="propagateSelected"
@@ -67,6 +78,7 @@ import { SplitData, useSplitter } from "@/composables/album/splitter";
 import Timeline from "primevue/timeline";
 import PhotoThumbPanelList from "./PhotoThumbPanelList.vue";
 import PhotoThumbPanelControl from "./PhotoThumbPanelControl.vue";
+import { isTouchDevice } from "@/utils/keybindings-utils";
 
 const lycheeStore = useLycheeStateStore();
 const { is_timeline_left_border_visible } = storeToRefs(lycheeStore);
@@ -87,7 +99,9 @@ const props = defineProps<{
 }>();
 
 const layout = ref(props.photoLayout);
-const isTimeline = ref(props.isTimeline);
+
+// We do not show the left border on touch devices (mostly phones) due to limited real estate.
+const isLeftBorderVisible = computed(() => is_timeline_left_border_visible && !isTouchDevice());
 
 // bubble up.
 const emits = defineEmits<{
@@ -117,4 +131,6 @@ const photosTimeLine = computed<SplitData<App.Http.Resources.Models.PhotoResourc
 		(p: App.Http.Resources.Models.PhotoResource) => p.timeline?.format ?? "Others",
 	),
 );
+
+const isTimeline = computed(() => props.isTimeline && photosTimeLine.value.length > 1);
 </script>
diff --git a/resources/js/components/gallery/albumModule/PhotoThumbPanelList.vue b/resources/js/components/gallery/albumModule/PhotoThumbPanelList.vue
index a8707ee2990..04ded16d2c9 100644
--- a/resources/js/components/gallery/albumModule/PhotoThumbPanelList.vue
+++ b/resources/js/components/gallery/albumModule/PhotoThumbPanelList.vue
@@ -1,6 +1,6 @@
 <template>
-	<div class="relative flex flex-wrap flex-row shrink w-full justify-start align-top" :id="'photoListing' + props.iter">
-		<template v-for="(photo, idx) in props.photos">
+	<div class="relative flex flex-wrap flex-row shrink w-full justify-start align-top" :id="'photoListing' + props.idx">
+		<template v-for="(photo, idx) in props.photos" :key="photo.id">
 			<PhotoThumb
 				@click="maySelect(idx + iter, $event)"
 				@contextmenu.prevent="menuOpen(idx + iter, $event)"
@@ -30,6 +30,7 @@ const props = defineProps<{
 	galleryConfig: App.Http.Resources.GalleryConfigs.PhotoLayoutConfig;
 	selectedPhotos: string[];
 	iter: number;
+	idx: number;
 }>();
 
 const lycheeStore = useLycheeStateStore();
@@ -57,7 +58,7 @@ const maySelect = (idx: number, e: MouseEvent) => {
 const menuOpen = (idx: number, e: MouseEvent) => emits("contexted", idx, e);
 
 // Layouts stuff
-const { activateLayout } = useLayouts(props.galleryConfig, layout, timelineData, "photoListing" + props.iter);
+const { activateLayout } = useLayouts(props.galleryConfig, layout, timelineData, "photoListing" + props.idx);
 onMounted(() => activateLayout());
 onUpdated(() => activateLayout());
 </script>
diff --git a/resources/js/components/gallery/searchModule/ResultPanel.vue b/resources/js/components/gallery/searchModule/ResultPanel.vue
index d3df9e0ca94..87c2a8e85f0 100644
--- a/resources/js/components/gallery/searchModule/ResultPanel.vue
+++ b/resources/js/components/gallery/searchModule/ResultPanel.vue
@@ -24,6 +24,7 @@
 		:album="undefined"
 		:gallery-config="props.layoutConfig"
 		:selected-photos="selectedPhotosIds"
+		@clicked="photoClick"
 		@selected="photoSelect"
 		@contexted="photoMenuOpen"
 		:is-timeline="props.isPhotoTimelineEnabled"
@@ -59,11 +60,13 @@ import { AlbumThumbConfig } from "../albumModule/thumbs/AlbumThumb.vue";
 import ContextMenu from "primevue/contextmenu";
 import Divider from "primevue/divider";
 import Paginator from "primevue/paginator";
+import { useRouter } from "vue-router";
+import { usePhotoRoute } from "@/composables/photo/photoRoute";
 
 const lycheeStore = useLycheeStateStore();
 const { are_nsfw_visible } = storeToRefs(lycheeStore);
 const togglableStore = useTogglablesStateStore();
-const { is_full_screen } = storeToRefs(togglableStore);
+const router = useRouter();
 
 const props = defineProps<{
 	// results
@@ -91,6 +94,12 @@ const emits = defineEmits<{
 	refresh: [];
 }>();
 
+const { photoRoute } = usePhotoRoute(router);
+
+function photoClick(idx: number, e: MouseEvent) {
+	router.push(photoRoute(photos.value[idx].id));
+}
+
 const photos = ref(props.photos);
 const children = ref(props.albums);
 
diff --git a/resources/js/components/headers/AlbumHeader.vue b/resources/js/components/headers/AlbumHeader.vue
index 5bac1d3b3c3..37cf89dee16 100644
--- a/resources/js/components/headers/AlbumHeader.vue
+++ b/resources/js/components/headers/AlbumHeader.vue
@@ -13,7 +13,7 @@
 		<template #end>
 			<router-link
 				:to="{ name: 'favourites' }"
-				v-if="(favourites.photos?.length ?? 0) > 0"
+				v-if="is_favourite_enabled && (favourites.photos?.length ?? 0) > 0"
 				class="hidden sm:block"
 				v-tooltip.bottom="'Favourites'"
 			>
@@ -80,7 +80,7 @@ const lycheeStore = useLycheeStateStore();
 lycheeStore.init();
 const favourites = useFavouriteStore();
 
-const { dropbox_api_key } = storeToRefs(lycheeStore);
+const { dropbox_api_key, is_favourite_enabled } = storeToRefs(lycheeStore);
 const { is_album_edit_open } = storeToRefs(togglableStore);
 
 const { toggleCreateAlbum, is_import_from_link_open, toggleImportFromLink, is_import_from_dropbox_open, toggleImportFromDropbox, toggleUpload } =
diff --git a/resources/js/components/headers/AlbumsHeader.vue b/resources/js/components/headers/AlbumsHeader.vue
index ad3bec51aa1..4f41ec4b477 100644
--- a/resources/js/components/headers/AlbumsHeader.vue
+++ b/resources/js/components/headers/AlbumsHeader.vue
@@ -84,7 +84,7 @@ import Toolbar from "primevue/toolbar";
 import ContextMenu from "primevue/contextmenu";
 import Divider from "primevue/divider";
 import ImportFromLink from "@/components/modals/ImportFromLink.vue";
-import { computed, ComputedRef, ref } from "vue";
+import { computed, ComputedRef } from "vue";
 import { onKeyStroke } from "@vueuse/core";
 import { useLycheeStateStore } from "@/stores/LycheeState";
 import { isTouchDevice, shouldIgnoreKeystroke } from "@/utils/keybindings-utils";
@@ -124,7 +124,7 @@ const lycheeStore = useLycheeStateStore();
 const togglableStore = useTogglablesStateStore();
 const favourites = useFavouriteStore();
 
-const { dropbox_api_key } = storeToRefs(lycheeStore);
+const { dropbox_api_key, is_favourite_enabled } = storeToRefs(lycheeStore);
 const { is_login_open, is_upload_visible, is_create_album_visible, is_create_tag_album_visible } = storeToRefs(togglableStore);
 
 const router = useRouter();
@@ -222,7 +222,7 @@ const menu = computed(() =>
 			to: { name: "favourites" },
 			type: "link",
 			icon: "pi pi-heart",
-			if: (favourites.photos?.length ?? 0) > 0,
+			if: is_favourite_enabled.value && (favourites.photos?.length ?? 0) > 0,
 		},
 		{
 			icon: "pi pi-search",
diff --git a/resources/js/composables/photo/photoRoute.ts b/resources/js/composables/photo/photoRoute.ts
index f04aff38a62..a7400bb1147 100644
--- a/resources/js/composables/photo/photoRoute.ts
+++ b/resources/js/composables/photo/photoRoute.ts
@@ -4,11 +4,12 @@ import { Router } from "vue-router";
 export function usePhotoRoute(router: Router) {
 	function photoRoute(photoId: string) {
 		const currentRoute = router.currentRoute.value.name as string;
+		const albumId = router.currentRoute.value.params.albumId as string | undefined;
+
 		if (currentRoute.startsWith("search")) {
-			return { name: "search", params: { albumId: router.currentRoute.value.params.albumId ?? ALL, photoId: photoId } };
+			return { name: "search", params: { albumId: albumId ?? ALL, photoId: photoId } };
 		}
 
-		const albumId = router.currentRoute.value.params.albumId as string | undefined;
 		return { name: "album", params: { albumId: albumId ?? ALL, photoId: photoId } };
 	}
 
diff --git a/resources/js/layouts/PhotoLayout.ts b/resources/js/layouts/PhotoLayout.ts
index c29634eeda2..64b668a1a3c 100644
--- a/resources/js/layouts/PhotoLayout.ts
+++ b/resources/js/layouts/PhotoLayout.ts
@@ -27,7 +27,7 @@ export function useLayouts(
 
 		switch (layout.value) {
 			case "square":
-				return useSquare(photoListing, configRef.value.photo_layout_square_column_width, configRef.value.photo_layout_gap);
+				return useSquare(photoListing, configRef.value.photo_layout_square_column_width, configRef.value.photo_layout_gap, timelineData);
 			case "justified":
 			case "unjustified":
 				return useJustify(photoListing, configRef.value.photo_layout_justified_row_height, timelineData);
@@ -60,7 +60,6 @@ export function useLayoutClass(layout: Ref<App.Enum.PhotoLayoutType>) {
 
 export function useGetLayoutConfig() {
 	const layoutConfig = ref(undefined) as Ref<undefined | App.Http.Resources.GalleryConfigs.PhotoLayoutConfig>;
-	const layout = ref("square") as Ref<App.Enum.PhotoLayoutType>;
 
 	function loadLayoutConfig(): Promise<void> {
 		return AlbumService.getLayout().then((data) => {
@@ -69,7 +68,6 @@ export function useGetLayoutConfig() {
 	}
 
 	return {
-		layout,
 		layoutConfig,
 		loadLayoutConfig,
 	};
diff --git a/resources/js/layouts/getWidth.ts b/resources/js/layouts/getWidth.ts
new file mode 100644
index 00000000000..b9769024c1f
--- /dev/null
+++ b/resources/js/layouts/getWidth.ts
@@ -0,0 +1,28 @@
+import { isTouchDevice } from "@/utils/keybindings-utils";
+import { TimelineData } from "./PhotoLayout";
+import { useRoute } from "vue-router";
+
+export function getWidth(timelineData: TimelineData): number {
+	const baseWidth = window.innerWidth;
+	const paddingLeftRight = 2 * 18;
+
+	let scrollBarWidth = 15;
+	if (isTouchDevice()) {
+		scrollBarWidth = 0;
+	}
+
+	const width = Math.min(baseWidth - paddingLeftRight - scrollBarWidth);
+
+	let timeLineBorder = 0;
+	if (timelineData.isTimeline.value === true && (timelineData.isLeftBorderVisible.value && !isTouchDevice()) === true) {
+		timeLineBorder = 50;
+	}
+
+	const route = useRoute();
+	const routeName = route.name as string;
+	if (routeName.includes("timeline")) {
+		timeLineBorder = 50;
+	}
+
+	return width - timeLineBorder;
+}
diff --git a/resources/js/layouts/useJustify.ts b/resources/js/layouts/useJustify.ts
index 111a20e2fc3..fdb87c6833a 100644
--- a/resources/js/layouts/useJustify.ts
+++ b/resources/js/layouts/useJustify.ts
@@ -1,7 +1,7 @@
 import { TimelineData } from "./PhotoLayout";
 import { ChildNodeWithDataStyle } from "./types";
 import createJustifiedLayout from "justified-layout";
-import { isTouchDevice } from "@/utils/keybindings-utils";
+import { getWidth } from "./getWidth";
 
 export function useJustify(el: HTMLElement, photoDefaultHeight: number = 320, timelineData: TimelineData) {
 	const width = getWidth(timelineData);
@@ -35,22 +35,3 @@ export function useJustify(el: HTMLElement, photoDefaultHeight: number = 320, ti
 		e.style.left = layoutGeometry.boxes[i].left + "px";
 	});
 }
-
-function getWidth(timelineData: TimelineData): number {
-	const baseWidth = window.innerWidth;
-	const paddingLeftRight = 2 * 18;
-
-	let scrollBarWidth = 15;
-	if (isTouchDevice()) {
-		scrollBarWidth = 0;
-	}
-
-	const width = Math.min(baseWidth - paddingLeftRight - scrollBarWidth);
-
-	let timeLineBorder = 0;
-	if (timelineData.isTimeline.value === true && timelineData.isLeftBorderVisible.value === true) {
-		timeLineBorder = 50;
-	}
-
-	return width - timeLineBorder;
-}
diff --git a/resources/js/layouts/useSquare.ts b/resources/js/layouts/useSquare.ts
index 07c6663a31d..dc26ccdadb2 100644
--- a/resources/js/layouts/useSquare.ts
+++ b/resources/js/layouts/useSquare.ts
@@ -1,10 +1,12 @@
+import { getWidth } from "./getWidth";
+import { TimelineData } from "./PhotoLayout";
 import { Column } from "./types";
 
-export function useSquare(el: HTMLElement, target_width_height: number, grid_gap: number = 12) {
+export function useSquare(el: HTMLElement, target_width_height: number, grid_gap: number = 12, timelineData: TimelineData) {
 	// @ts-expect-error
 	const gridItems: ChildNodeWithDataStyle[] = [...el.childNodes].filter((gridItem) => gridItem.nodeType === 1);
 
-	const max_width = parseInt(getComputedStyle(el).width);
+	const max_width = getWidth(timelineData);
 	const usable_width = max_width;
 	const perChunk = Math.floor((usable_width + grid_gap) / target_width_height);
 	const remaining_space = usable_width - perChunk * target_width_height - (perChunk - 1) * grid_gap;
diff --git a/resources/js/menus/LeftMenu.vue b/resources/js/menus/LeftMenu.vue
index e80630f2204..501253b9756 100644
--- a/resources/js/menus/LeftMenu.vue
+++ b/resources/js/menus/LeftMenu.vue
@@ -1,7 +1,7 @@
 <template>
 	<Drawer v-model:visible="left_menu_open" :pt:content:class="'flex flex-col justify-start gap-10'">
 		<template #header>
-			<div v-if="user?.id === null" class="flex items-center gap-2 text-muted-color hover:text-primary-400 w-full">
+			<div v-if="user?.id === null && isGallery" class="flex items-center gap-2 text-muted-color hover:text-primary-400 w-full">
 				<Button class="border-none w-full text-left justify-start" severity="secondary" text @click="togglableStore.toggleLogin()">
 					<i class="pi pi-sign-in text-lg mr-2" />
 					{{ $t("left-menu.login") }}
diff --git a/resources/js/views/gallery-panels/Album.vue b/resources/js/views/gallery-panels/Album.vue
index c80a0cd6c2e..91cf3fb5a71 100644
--- a/resources/js/views/gallery-panels/Album.vue
+++ b/resources/js/views/gallery-panels/Album.vue
@@ -237,7 +237,7 @@ function toggleSlideShow() {
 	}
 
 	slideshow();
-	router.push({ name: "photo", params: { albumId: album.value.id, photoId: album.value.photos[0].id } });
+	router.push({ name: "album", params: { albumId: album.value.id, photoId: album.value.photos[0].id } });
 }
 
 const { layoutConfig, loadLayoutConfig } = useGetLayoutConfig();
diff --git a/resources/js/views/gallery-panels/Favourites.vue b/resources/js/views/gallery-panels/Favourites.vue
index dc2f49233ef..3b6bf34af32 100644
--- a/resources/js/views/gallery-panels/Favourites.vue
+++ b/resources/js/views/gallery-panels/Favourites.vue
@@ -87,7 +87,7 @@ const {
 } = useSelection(photos, children, togglableStore);
 
 function photoClick(idx: number, e: MouseEvent) {
-	router.push({ name: "photo", params: { albumId: photos.value[idx].album_id ?? ALL, photoId: photos.value[idx].id } });
+	router.push({ name: "album", params: { albumId: photos.value[idx].album_id ?? ALL, photoId: photos.value[idx].id } });
 }
 
 onKeyStroke("f", () => !shouldIgnoreKeystroke() && togglableStore.toggleFullScreen());
diff --git a/resources/js/views/gallery-panels/Frame.vue b/resources/js/views/gallery-panels/Frame.vue
index 6ed6aad9b55..a7bb0d2f8f3 100644
--- a/resources/js/views/gallery-panels/Frame.vue
+++ b/resources/js/views/gallery-panels/Frame.vue
@@ -66,15 +66,23 @@ onMounted(() => {
 });
 
 onUnmounted(() => {
-	document.exitFullscreen();
+	try {
+		document.exitFullscreen();
+	} catch (_) {
+		// Do nothing.
+	}
 	clearTimeouts();
 });
 
 function goBack() {
 	clearTimeouts();
-	document.exitFullscreen();
+	try {
+		document.exitFullscreen();
+	} catch (_) {
+		// Do nothing.
+	}
 
-	if (props.albumId !== undefined) {
+	if (props.albumId !== undefined && props.albumId !== "") {
 		router.push({ name: "album", params: { albumId: props.albumId } });
 	} else {
 		router.push({ name: "gallery" });
diff --git a/resources/js/views/gallery-panels/Map.vue b/resources/js/views/gallery-panels/Map.vue
index 77a1a984860..41ec382399b 100644
--- a/resources/js/views/gallery-panels/Map.vue
+++ b/resources/js/views/gallery-panels/Map.vue
@@ -67,7 +67,7 @@ const lycheeStore = useLycheeStateStore();
 lycheeStore.init();
 
 function goBack() {
-	if (props.albumId !== undefined) {
+	if (props.albumId !== undefined && props.albumId !== "") {
 		router.push({ name: "album", params: { albumId: props.albumId } });
 	} else {
 		router.push({ name: "gallery" });
@@ -92,7 +92,7 @@ function loadMapProvider() {
 }
 
 function mapInit() {
-	// @ts-ignore
+	// @ts-expect-error We don't care about the default icon.
 	delete L.Icon.Default.prototype._getIconUrl;
 	L.Icon.Default.mergeOptions({
 		iconRetinaUrl: Constants.BASE_URL + "/img/marker-icon-2x.png",
diff --git a/resources/js/views/gallery-panels/Search.vue b/resources/js/views/gallery-panels/Search.vue
index e169e3304c7..ba298a5e33f 100644
--- a/resources/js/views/gallery-panels/Search.vue
+++ b/resources/js/views/gallery-panels/Search.vue
@@ -340,7 +340,7 @@ function goBack() {
 		return;
 	}
 
-	if (albumId.value !== undefined && albumId.value !== ALL) {
+	if (albumId.value !== undefined && albumId.value !== ALL && albumId.value !== "") {
 		router.push({ name: "album", params: { albumId: props.albumId } });
 	} else {
 		router.push({ name: "gallery" });


Test patch: