mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat: search results page (without keyboard actions)
fix: missing responsive calculation in UserPageLayout
This commit is contained in:
parent
398755e65e
commit
0ebf75a96f
15 changed files with 570 additions and 262 deletions
|
|
@ -49,7 +49,7 @@
|
|||
<div
|
||||
tabindex="-1"
|
||||
class="relative z-0 grid grid-cols-[--spacing(0)_auto] overflow-hidden sidebar:grid-cols-[--spacing(64)_auto]
|
||||
{hideNavbar ? 'h-dvh' : 'h-[calc(100dvh-var(--navbar-height))]'}
|
||||
{hideNavbar ? 'h-dvh' : 'h-[calc(100dvh-var(--navbar-height))] max-md:h-[calc(100dvh-var(--navbar-height-md))]'}
|
||||
{hideNavbar ? 'pt-(--navbar-height)' : ''}
|
||||
{hideNavbar ? 'max-md:pt-(--navbar-height-md)' : ''}"
|
||||
>
|
||||
|
|
|
|||
91
web/src/lib/components/search/SearchResults.svelte
Normal file
91
web/src/lib/components/search/SearchResults.svelte
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
<script lang="ts">
|
||||
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
||||
import SearchResultsAssetViewer from '$lib/components/search/SearchResultsAssetViewer.svelte';
|
||||
|
||||
import AssetLayout from '$lib/components/timeline/AssetLayout.svelte';
|
||||
import Photostream from '$lib/components/timeline/Photostream.svelte';
|
||||
import SelectableSegment from '$lib/components/timeline/SelectableSegment.svelte';
|
||||
import StreamWithViewer from '$lib/components/timeline/StreamWithViewer.svelte';
|
||||
import Skeleton from '$lib/elements/Skeleton.svelte';
|
||||
import { SearchResultsManager } from '$lib/managers/searchresults-manager/SearchResultsManager.svelte';
|
||||
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
interface Props {
|
||||
assetInteraction: AssetInteraction;
|
||||
children?: Snippet<[]>;
|
||||
|
||||
stylePaddingHorizontalPx?: number;
|
||||
styleMarginTopPx?: number;
|
||||
searchResultsManager: SearchResultsManager;
|
||||
}
|
||||
|
||||
let { searchResultsManager, assetInteraction, children, stylePaddingHorizontalPx, styleMarginTopPx }: Props =
|
||||
$props();
|
||||
let viewer: Photostream | undefined = $state(undefined);
|
||||
|
||||
const onAfterNavigateComplete = ({ scrollToAssetQueryParam }: { scrollToAssetQueryParam: boolean }) =>
|
||||
viewer?.completeAfterNavigate({ scrollToAssetQueryParam });
|
||||
</script>
|
||||
|
||||
<StreamWithViewer timelineManager={searchResultsManager} {onAfterNavigateComplete}>
|
||||
{#snippet assetViewer({ onViewerClose })}
|
||||
<SearchResultsAssetViewer timelineManager={searchResultsManager} {onViewerClose} />
|
||||
{/snippet}
|
||||
<Photostream
|
||||
bind:this={viewer}
|
||||
{stylePaddingHorizontalPx}
|
||||
{styleMarginTopPx}
|
||||
showScrollbar={true}
|
||||
alwaysShowScrollbar={true}
|
||||
enableRouting={true}
|
||||
timelineManager={searchResultsManager}
|
||||
isShowDeleteConfirmation={true}
|
||||
smallHeaderHeight={{ rowHeight: 100, headerHeight: 2 }}
|
||||
largeHeaderHeight={{ rowHeight: 235, headerHeight: 2 }}
|
||||
>
|
||||
{@render children?.()}
|
||||
|
||||
{#snippet skeleton({ segment })}
|
||||
<Skeleton height={segment.height - segment.timelineManager.headerHeight} />
|
||||
{/snippet}
|
||||
|
||||
{#snippet segment({ segment, onScrollCompensationMonthInDOM })}
|
||||
<SelectableSegment
|
||||
{segment}
|
||||
{onScrollCompensationMonthInDOM}
|
||||
timelineManager={searchResultsManager}
|
||||
{assetInteraction}
|
||||
isSelectionMode={false}
|
||||
singleSelect={false}
|
||||
>
|
||||
{#snippet content({ onAssetOpen, onAssetSelect, onAssetHover })}
|
||||
<AssetLayout
|
||||
photostreamManager={searchResultsManager}
|
||||
viewerAssets={segment.viewerAssets}
|
||||
height={segment.height}
|
||||
width={searchResultsManager.viewportWidth}
|
||||
>
|
||||
{#snippet thumbnail({ asset, position })}
|
||||
{@const isAssetSelectionCandidate = assetInteraction.hasSelectionCandidate(asset.id)}
|
||||
{@const isAssetSelected = assetInteraction.hasSelectedAsset(asset.id)}
|
||||
<Thumbnail
|
||||
showStackedIcon={true}
|
||||
showArchiveIcon={true}
|
||||
{asset}
|
||||
onClick={() => onAssetOpen(asset)}
|
||||
onSelect={() => onAssetSelect(asset)}
|
||||
onMouseEvent={() => onAssetHover(asset)}
|
||||
selected={isAssetSelected}
|
||||
selectionCandidate={isAssetSelectionCandidate}
|
||||
thumbnailWidth={position.width}
|
||||
thumbnailHeight={position.height}
|
||||
/>
|
||||
{/snippet}
|
||||
</AssetLayout>
|
||||
{/snippet}
|
||||
</SelectableSegment>
|
||||
{/snippet}
|
||||
</Photostream>
|
||||
</StreamWithViewer>
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
||||
import { AppRoute, AssetAction } from '$lib/constants';
|
||||
import { SearchResultsManager } from '$lib/managers/searchresults-manager/SearchResultsManager.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
let { asset: viewingAsset, setAssetId, preloadAssets } = assetViewingStore;
|
||||
|
||||
interface Props {
|
||||
timelineManager: SearchResultsManager;
|
||||
|
||||
onViewerClose?: (asset: { id: string }) => Promise<void>;
|
||||
}
|
||||
|
||||
let { timelineManager, onViewerClose = () => Promise.resolve(void 0) }: Props = $props();
|
||||
|
||||
const handleNext = async (): Promise<boolean> => {
|
||||
const next = timelineManager.findNextAsset($viewingAsset.id);
|
||||
if (next) {
|
||||
await navigateToAsset(next);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleRandom = async (): Promise<{ id: string } | undefined> => {
|
||||
const next = timelineManager.findRandomAsset();
|
||||
if (next) {
|
||||
await navigateToAsset(next);
|
||||
return { id: next.id };
|
||||
}
|
||||
};
|
||||
|
||||
const handlePrevious = async (): Promise<boolean> => {
|
||||
const next = timelineManager.findPreviousAsset($viewingAsset.id);
|
||||
if (next) {
|
||||
await navigateToAsset(next);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const navigateToAsset = async (asset?: { id: string }) => {
|
||||
if (asset && asset.id !== $viewingAsset.id) {
|
||||
await setAssetId(asset.id);
|
||||
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
||||
}
|
||||
};
|
||||
|
||||
const handleAction = async (action: Action) => {
|
||||
switch (action.type) {
|
||||
case AssetAction.ARCHIVE:
|
||||
case AssetAction.DELETE:
|
||||
case AssetAction.TRASH: {
|
||||
timelineManager.removeAssets([action.asset.id]);
|
||||
if (!(await handlePrevious())) {
|
||||
await goto(AppRoute.PHOTOS);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#await import('../asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
||||
<AssetViewer
|
||||
asset={$viewingAsset}
|
||||
preloadAssets={$preloadAssets}
|
||||
onAction={handleAction}
|
||||
onPrevious={handlePrevious}
|
||||
onNext={handleNext}
|
||||
onRandom={handleRandom}
|
||||
onClose={onViewerClose}
|
||||
/>
|
||||
{/await}
|
||||
|
|
@ -23,6 +23,7 @@
|
|||
[
|
||||
{
|
||||
segment: PhotostreamSegment;
|
||||
stylePaddingHorizontalPx: number;
|
||||
},
|
||||
]
|
||||
>;
|
||||
|
|
@ -37,7 +38,6 @@
|
|||
alwaysShowScrollbar?: boolean;
|
||||
showSkeleton?: boolean;
|
||||
isShowDeleteConfirmation?: boolean;
|
||||
styleMarginRightOverride?: string;
|
||||
|
||||
header?: Snippet<[scrollToFunction: (top: number) => void]>;
|
||||
children?: Snippet;
|
||||
|
|
@ -53,8 +53,9 @@
|
|||
rowHeight: number;
|
||||
headerHeight: number;
|
||||
};
|
||||
styleMarginContentHorizontal?: string;
|
||||
styleMarginTop?: string;
|
||||
stylePaddingHorizontalPx?: number;
|
||||
styleMarginTopPx?: number;
|
||||
styleMarginRightPx?: number;
|
||||
}
|
||||
|
||||
let {
|
||||
|
|
@ -64,9 +65,9 @@
|
|||
timelineManager = $bindable(),
|
||||
showSkeleton = $bindable(true),
|
||||
showScrollbar,
|
||||
styleMarginRightOverride,
|
||||
styleMarginContentHorizontal = '0px',
|
||||
styleMarginTop = '0px',
|
||||
styleMarginRightPx = 0,
|
||||
stylePaddingHorizontalPx = 0,
|
||||
styleMarginTopPx = 0,
|
||||
alwaysShowScrollbar,
|
||||
|
||||
isShowDeleteConfirmation = $bindable(false),
|
||||
|
|
@ -221,23 +222,26 @@
|
|||
{ 'm-0': isEmpty },
|
||||
{ 'ms-0': !isEmpty },
|
||||
]}
|
||||
style:height={`calc(100% - ${styleMarginTop})`}
|
||||
style:margin-top={styleMarginTop}
|
||||
style:margin-right={styleMarginRightOverride}
|
||||
style:height={`calc(100% - ${styleMarginTopPx}px)`}
|
||||
style:margin-top={styleMarginTopPx + 'px'}
|
||||
style:margin-right={styleMarginRightPx + 'px'}
|
||||
style:padding-left={stylePaddingHorizontalPx + 'px'}
|
||||
style:padding-right={stylePaddingHorizontalPx + 'px'}
|
||||
style:scrollbar-width={showScrollbar ? 'thin' : 'none'}
|
||||
tabindex="-1"
|
||||
bind:clientHeight={timelineManager.viewportHeight}
|
||||
bind:clientWidth={
|
||||
null, (v: number) => ((timelineManager.viewportWidth = v - stylePaddingHorizontalPx * 2), updateSlidingWindow())
|
||||
}
|
||||
bind:this={element}
|
||||
onscroll={() => (handleTimelineScroll(), updateSlidingWindow(), updateIsScrolling())}
|
||||
>
|
||||
<section
|
||||
bind:this={timelineElement}
|
||||
id="virtual-timeline"
|
||||
style:margin-left={styleMarginContentHorizontal}
|
||||
style:margin-right={styleMarginContentHorizontal}
|
||||
class:relative={true}
|
||||
class:invisible={showSkeleton}
|
||||
style:height={timelineManager.timelineHeight + 'px'}
|
||||
bind:clientWidth={null, (v: number) => ((timelineManager.viewportWidth = v), updateSlidingWindow())}
|
||||
>
|
||||
<section
|
||||
use:resizeObserver={topSectionResizeObserver}
|
||||
|
|
@ -252,7 +256,6 @@
|
|||
{@render empty?.()}
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
{#each timelineManager.months as monthGroup (monthGroup.id)}
|
||||
{@const shouldDisplay = monthGroup.intersecting && monthGroup.isLoaded}
|
||||
{@const absoluteHeight = monthGroup.top}
|
||||
|
|
@ -266,7 +269,7 @@
|
|||
style:width="100%"
|
||||
>
|
||||
{#if !shouldDisplay}
|
||||
{@render skeleton({ segment: monthGroup })}
|
||||
{@render skeleton({ segment: monthGroup, stylePaddingHorizontalPx })}
|
||||
{:else}
|
||||
{@render segment({
|
||||
segment: monthGroup,
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@
|
|||
{timelineManager}
|
||||
{showSkeleton}
|
||||
{isShowDeleteConfirmation}
|
||||
styleMarginRightOverride={scrubberWidth + 'px'}
|
||||
styleMarginRightPx={scrubberWidth}
|
||||
{handleTimelineScroll}
|
||||
{segment}
|
||||
{skeleton}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,10 @@
|
|||
menuItem?: boolean;
|
||||
unarchive?: boolean;
|
||||
manager?: PhotostreamManager;
|
||||
removeOnArchive?: boolean;
|
||||
}
|
||||
|
||||
let { onArchive, menuItem = false, unarchive = false, manager }: Props = $props();
|
||||
let { onArchive, menuItem = false, unarchive = false, manager, removeOnArchive }: Props = $props();
|
||||
|
||||
let text = $derived(unarchive ? $t('unarchive') : $t('to_archive'));
|
||||
let icon = $derived(unarchive ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline);
|
||||
|
|
@ -31,7 +32,12 @@
|
|||
loading = true;
|
||||
const ids = await archiveAssets(assets, visibility as AssetVisibility);
|
||||
if (ids) {
|
||||
manager?.updateAssetOperation(ids, (asset) => ((asset.visibility = visibility), void 0));
|
||||
manager?.updateAssetOperation(ids, (asset) => {
|
||||
asset.visibility = visibility;
|
||||
return {
|
||||
remove: removeOnArchive,
|
||||
};
|
||||
});
|
||||
onArchive?.(ids, visibility ? AssetVisibility.Archive : AssetVisibility.Timeline);
|
||||
clearSelect();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,9 +41,11 @@
|
|||
const assets = [...getOwnedAssets()];
|
||||
const undo = (assets: TimelineAsset[]) => {
|
||||
manager?.upsertAssets(assets);
|
||||
manager?.order(assetOrdering ?? []);
|
||||
onUndoDelete?.(assets);
|
||||
};
|
||||
await deleteAssets(force, onAssetDelete, assets, undo);
|
||||
const assetOrdering = manager?.assets.map((asset) => asset.id);
|
||||
manager?.removeAssets(assets.map((asset) => asset.id));
|
||||
clearSelect();
|
||||
isShowConfirmation = false;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue