diff --git a/web/src/lib/components/shared-components/tree/breadcrumbs.svelte b/web/src/lib/components/shared-components/tree/breadcrumbs.svelte new file mode 100644 index 0000000000..a3c49a1430 --- /dev/null +++ b/web/src/lib/components/shared-components/tree/breadcrumbs.svelte @@ -0,0 +1,59 @@ +<script lang="ts"> + import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; + import Icon from '$lib/components/elements/icon.svelte'; + import { mdiArrowUpLeft, mdiChevronRight } from '@mdi/js'; + import { t } from 'svelte-i18n'; + + export let pathSegments: string[] = []; + export let getLink: (path: string) => string; + export let title: string; + export let icon: string; + + $: isRoot = pathSegments.length === 0; +</script> + +<nav class="flex items-center py-2"> + {#if !isRoot} + <div> + <CircleIconButton + icon={mdiArrowUpLeft} + title={$t('to_parent')} + href={getLink(pathSegments.slice(0, -1).join('/'))} + class="mr-2" + padding="2" + /> + </div> + {/if} + + <div + class="bg-gray-50 dark:bg-immich-dark-gray/50 w-full p-2 rounded-2xl border border-gray-100 dark:border-gray-900 overflow-y-auto immich-scrollbar" + > + <ol class="flex gap-2 items-center"> + <li> + <CircleIconButton + {icon} + href={getLink('')} + {title} + size="1.25em" + padding="2" + aria-current={isRoot ? 'page' : undefined} + /> + </li> + {#each pathSegments as segment, index} + {@const isLastSegment = index === pathSegments.length - 1} + <li + class="flex gap-2 items-center font-mono text-sm text-nowrap text-immich-primary dark:text-immich-dark-primary" + > + <Icon path={mdiChevronRight} class="text-gray-500 dark:text-gray-300" size={16} ariaHidden /> + {#if isLastSegment} + <p class="cursor-default">{segment}</p> + {:else} + <a class="underline hover:font-semibold" href={getLink(pathSegments.slice(0, index + 1).join('/'))}> + {segment} + </a> + {/if} + </li> + {/each} + </ol> + </div> +</nav> diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index d0d330480a..25f3b6ea2f 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -1191,7 +1191,7 @@ "to_change_password": "Change password", "to_favorite": "Favorite", "to_login": "Login", - "to_root": "To root", + "to_parent": "Go to parent", "to_trash": "Trash", "toggle_settings": "Toggle settings", "toggle_theme": "Toggle dark theme", diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index a8b8602c02..1ffa64d3a3 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -1,8 +1,6 @@ <script lang="ts"> import { goto } from '$app/navigation'; import { page } from '$app/stores'; - import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; - import Icon from '$lib/components/elements/icon.svelte'; import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte'; import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte'; import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte'; @@ -13,10 +11,11 @@ import { foldersStore } from '$lib/stores/folders.store'; import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils'; import { type AssetResponseDto } from '@immich/sdk'; - import { mdiArrowUpLeft, mdiChevronRight, mdiFolder, mdiFolderHome, mdiFolderOutline } from '@mdi/js'; + import { mdiFolder, mdiFolderHome, mdiFolderOutline } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; + import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte'; export let data: PageData; @@ -35,20 +34,11 @@ await navigateToView(normalizeTreePath(`${data.path || ''}/${folderName}`)); }; - const handleBackNavigation = async () => { - if (data.path) { - const parentPath = data.path.split('/').slice(0, -1).join('/'); - await navigateToView(parentPath); - } - }; - - const handleBreadcrumbNavigation = async (targetPath: string) => { - await navigateToView(targetPath); - }; - const getLink = (path: string) => { const url = new URL(AppRoute.FOLDERS, window.location.href); - url.searchParams.set(QueryParameter.PATH, path); + if (path) { + url.searchParams.set(QueryParameter.PATH, path); + } return url.href; }; @@ -70,33 +60,7 @@ </section> </SideBarSection> - <section id="path-summary" class="text-immich-primary dark:text-immich-dark-primary rounded-xl flex"> - {#if data.path} - <CircleIconButton icon={mdiArrowUpLeft} title="Back" on:click={handleBackNavigation} class="mr-2" padding="2" /> - {/if} - - <div - class="flex place-items-center gap-2 bg-gray-50 dark:bg-immich-dark-gray/50 w-full py-2 px-4 rounded-2xl border border-gray-100 dark:border-gray-900" - > - <a href={`${AppRoute.FOLDERS}`} title={$t('to_root')}> - <Icon path={mdiFolderHome} class="text-immich-primary dark:text-immich-dark-primary mr-2" size={28} /> - </a> - {#each pathSegments as segment, index} - <button - class="text-sm font-mono underline hover:font-semibold" - on:click={() => handleBreadcrumbNavigation(pathSegments.slice(0, index + 1).join('/'))} - type="button" - > - {segment} - </button> - <p class="text-gray-500"> - {#if index < pathSegments.length - 1} - <Icon path={mdiChevronRight} class="text-gray-500 dark:text-gray-300" size={16} /> - {/if} - </p> - {/each} - </div> - </section> + <Breadcrumbs {pathSegments} icon={mdiFolderHome} title={$t('folders')} {getLink} /> <section class="mt-2"> <TreeItemThumbnails items={data.currentFolders} icon={mdiFolder} onClick={handleNavigation} /> diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte index 7335bf83c1..30f17da15d 100644 --- a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -22,10 +22,11 @@ import { AssetStore } from '$lib/stores/assets.store'; import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils'; import { deleteTag, getAllTags, updateTag, upsertTags, type TagResponseDto } from '@immich/sdk'; - import { mdiChevronRight, mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js'; + import { mdiPencil, mdiPlus, mdiTag, mdiTagMultiple, mdiTrashCanOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; import { dialogController } from '$lib/components/shared-components/dialog/dialog'; + import Breadcrumbs from '$lib/components/shared-components/tree/breadcrumbs.svelte'; export let data: PageData; @@ -47,13 +48,11 @@ await navigateToView(normalizeTreePath(`${data.path || ''}/${tag}`)); }; - const handleBreadcrumbNavigation = async (targetPath: string) => { - await navigateToView(targetPath); - }; - const getLink = (path: string) => { const url = new URL(AppRoute.TAGS, window.location.href); - url.searchParams.set(QueryParameter.PATH, path); + if (path) { + url.searchParams.set(QueryParameter.PATH, path); + } return url.href; }; @@ -149,14 +148,13 @@ </div> </LinkButton> - <LinkButton on:click={handleEdit}> - <div class="flex place-items-center gap-2 text-sm"> - <Icon path={mdiPencil} size="18" /> - <p class="hidden md:block">{$t('edit_tag')}</p> - </div> - </LinkButton> - {#if pathSegments.length > 0 && tag} + <LinkButton on:click={handleEdit}> + <div class="flex place-items-center gap-2 text-sm"> + <Icon path={mdiPencil} size="18" /> + <p class="hidden md:block">{$t('edit_tag')}</p> + </div> + </LinkButton> <LinkButton on:click={handleDelete}> <div class="flex place-items-center gap-2 text-sm"> <Icon path={mdiTrashCanOutline} size="18" /> @@ -166,27 +164,7 @@ {/if} </section> - <section - class="flex place-items-center gap-2 mt-2 text-immich-primary dark:text-immich-dark-primary rounded-2xl bg-gray-50 dark:bg-immich-dark-gray/50 w-full py-2 px-4 border border-gray-100 dark:border-gray-900" - > - <a href={`${AppRoute.TAGS}`} title={$t('to_root')}> - <Icon path={mdiTagMultiple} class="text-immich-primary dark:text-immich-dark-primary mr-2" size={28} /> - </a> - {#each pathSegments as segment, index} - <button - class="text-sm font-mono underline hover:font-semibold" - on:click={() => handleBreadcrumbNavigation(pathSegments.slice(0, index + 1).join('/'))} - type="button" - > - {segment} - </button> - <p class="text-gray-500"> - {#if index < pathSegments.length - 1} - <Icon path={mdiChevronRight} class="text-gray-500 dark:text-gray-300" size={16} /> - {/if} - </p> - {/each} - </section> + <Breadcrumbs {pathSegments} icon={mdiTagMultiple} title={$t('tags')} {getLink} /> <section class="mt-2 h-full"> {#key $page.url.href}