1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-01-27 22:22:45 +01:00

feat(web): Enable selection interactions in folder view ()

* feat(web): Enable selection interactions in folder view

* feat(web): Add link to parent folder in detail pane, if folders are enabled

* Added invalidation and refreshing of asset cache on changes

* fix: removed unused imports and changed link

* chore: styling

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Arno 2025-01-03 17:09:31 +01:00 committed by GitHub
parent 007caa26bd
commit b45ff8d09f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 96 additions and 13 deletions
i18n
web/src
lib
components/asset-viewer
stores
routes/(user)/folders/[[photos=photos]]/[[assetId=id]]

View file

@ -760,6 +760,7 @@
"getting_started": "Getting Started",
"go_back": "Go back",
"go_to_search": "Go to search",
"go_to_folder": "Go to folder",
"group_albums_by": "Group albums by...",
"group_no": "No grouping",
"group_owner": "Group by owner",
@ -1343,4 +1344,4 @@
"yes": "Yes",
"you_dont_have_any_shared_links": "You don't have any shared links",
"zoom_image": "Zoom Image"
}
}

View file

@ -132,6 +132,14 @@
showEditFaces = false;
};
const getAssetFolderHref = (asset: AssetResponseDto) => {
const folderUrl = new URL(AppRoute.FOLDERS, globalThis.location.href);
// Remove the last part of the path to get the parent path
const assetParentPath = asset.originalPath.split('/').slice(0, -1).join('/');
folderUrl.searchParams.set(QueryParameter.PATH, assetParentPath);
return folderUrl.href;
};
const toggleAssetPath = () => (showAssetPath = !showAssetPath);
let isShowChangeDate = $state(false);
@ -369,9 +377,14 @@
{/if}
</p>
{#if showAssetPath}
<p class="text-xs opacity-50 break-all pb-2" transition:slide={{ duration: 250 }}>
{asset.originalPath}
</p>
<a href={getAssetFolderHref(asset)} title={$t('go_to_folder')}>
<p
class="text-xs opacity-50 break-all pb-2 hover:dark:text-immich-dark-primary hover:text-immich-primary"
transition:slide={{ duration: 250 }}
>
{asset.originalPath}
</p>
</a>
{/if}
{#if (asset.exifInfo?.exifImageHeight && asset.exifInfo?.exifImageWidth) || asset.exifInfo?.fileSizeInByte}
<div class="flex gap-2 text-sm">

View file

@ -27,6 +27,17 @@ class FoldersStore {
this.uniquePaths.sort();
}
bustAssetCache() {
this.assets = {};
}
async refreshAssetsByPath(path: string | null) {
if (!path) {
return;
}
this.assets[path] = await getAssetsByOriginalPath({ path });
}
async fetchAssetsByPath(path: string) {
if (this.assets[path]) {
return;

View file

@ -1,5 +1,5 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { goto, invalidateAll } from '$app/navigation';
import { page } from '$app/stores';
import UserPageLayout, { headerId } from '$lib/components/layouts/user-page-layout.svelte';
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
@ -10,13 +10,28 @@
import type { Viewport } from '$lib/stores/assets.store';
import { foldersStore } from '$lib/stores/folders.svelte';
import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
import { mdiFolder, mdiFolderHome, mdiFolderOutline } from '@mdi/js';
import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } 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';
import SkipLink from '$lib/components/elements/buttons/skip-link.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import { preferences } from '$lib/stores/user.store';
import { cancelMultiselect } from '$lib/utils/asset-utils';
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
interface Props {
data: PageData;
@ -31,6 +46,8 @@
let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || '');
let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree));
$inspect(data).with(console.log);
const assetInteraction = new AssetInteraction();
onMount(async () => {
@ -50,8 +67,51 @@
};
const navigateToView = (path: string) => goto(getLink(path));
const triggerAssetUpdate = async () => {
await foldersStore.refreshAssetsByPath(data.path);
await invalidateAll();
};
const handleSelectAll = () => {
if (!data.pathAssets) {
return;
}
assetInteraction.selectAssets(data.pathAssets);
};
</script>
{#if assetInteraction.selectionActive}
<div class="fixed z-[910] top-0 left-0 w-full">
<AssetSelectControlBar
assets={assetInteraction.selectedAssets}
clearSelect={() => cancelMultiselect(assetInteraction)}
>
<CreateSharedLink />
<CircleIconButton title={$t('select_all')} icon={mdiSelectAll} onclick={handleSelectAll} />
<ButtonContextMenu icon={mdiPlus} title={$t('add_to')}>
<AddToAlbum />
<AddToAlbum shared />
</ButtonContextMenu>
<FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={triggerAssetUpdate} />
<ButtonContextMenu icon={mdiDotsVertical} title={$t('add')}>
<DownloadAction menuItem />
<ChangeDate menuItem />
<ChangeLocation menuItem />
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} onArchive={triggerAssetUpdate} />
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
<TagAction menuItem />
{/if}
<DeleteAssets menuItem onAssetDelete={triggerAssetUpdate} />
<hr />
<AssetJobActions />
</ButtonContextMenu>
</AssetSelectControlBar>
</div>
{/if}
<UserPageLayout title={data.meta.title}>
{#snippet sidebar()}
<SideBarSection>
@ -78,13 +138,7 @@
<!-- Assets -->
{#if data.pathAssets && data.pathAssets.length > 0}
<div bind:clientHeight={viewport.height} bind:clientWidth={viewport.width} class="mt-2">
<GalleryViewer
assets={data.pathAssets}
{assetInteraction}
{viewport}
disableAssetSelect={true}
showAssetName={true}
/>
<GalleryViewer assets={data.pathAssets} {assetInteraction} {viewport} showAssetName={true} />
</div>
{/if}
</section>

View file

@ -19,6 +19,10 @@ export const load = (async ({ params, url }) => {
if (path) {
await foldersStore.fetchAssetsByPath(path);
pathAssets = foldersStore.assets[path] || null;
} else {
// If no path is provided, we we're at the root level
// We should bust the asset cache of the folder store, to make sure we don't show stale data
foldersStore.bustAssetCache();
}
let tree = buildTree(foldersStore.uniquePaths);