mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +00:00
feat: photostream can have scrollbar, style options, standardize small/large layout sizes
- Add configurable header height props for responsive layouts (smallHeaderHeight/largeHeaderHeight) - Add style customization props: styleMarginContentHorizontal, styleMarginTop, alwaysShowScrollbar - Replace hardcoded layout values with configurable props - Change root element from <section> to custom <photostream> element for better semantic structure - Move viewport width binding to inner timeline element for more accurate measurements - Simplify HMR handler by removing file-specific checks - Add segment loading check to prevent rendering unloaded segments - Add spacing margin between month groups using layout options - Change scrollbar-width from 'auto' to 'thin' for consistency - Remove unused UpdatePayload type import
This commit is contained in:
parent
1b60c9d32f
commit
79adb016e8
1 changed files with 50 additions and 25 deletions
|
|
@ -8,7 +8,6 @@
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
import { onMount, type Snippet } from 'svelte';
|
import { onMount, type Snippet } from 'svelte';
|
||||||
import type { UpdatePayload } from 'vite';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
segment: Snippet<
|
segment: Snippet<
|
||||||
|
|
@ -35,6 +34,7 @@
|
||||||
enableRouting: boolean;
|
enableRouting: boolean;
|
||||||
timelineManager: PhotostreamManager;
|
timelineManager: PhotostreamManager;
|
||||||
|
|
||||||
|
alwaysShowScrollbar?: boolean;
|
||||||
showSkeleton?: boolean;
|
showSkeleton?: boolean;
|
||||||
isShowDeleteConfirmation?: boolean;
|
isShowDeleteConfirmation?: boolean;
|
||||||
styleMarginRightOverride?: string;
|
styleMarginRightOverride?: string;
|
||||||
|
|
@ -43,6 +43,18 @@
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
empty?: Snippet;
|
empty?: Snippet;
|
||||||
handleTimelineScroll?: () => void;
|
handleTimelineScroll?: () => void;
|
||||||
|
|
||||||
|
smallHeaderHeight?: {
|
||||||
|
rowHeight: number;
|
||||||
|
headerHeight: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
largeHeaderHeight?: {
|
||||||
|
rowHeight: number;
|
||||||
|
headerHeight: number;
|
||||||
|
};
|
||||||
|
styleMarginContentHorizontal?: string;
|
||||||
|
styleMarginTop?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
|
|
@ -51,14 +63,27 @@
|
||||||
enableRouting,
|
enableRouting,
|
||||||
timelineManager = $bindable(),
|
timelineManager = $bindable(),
|
||||||
showSkeleton = $bindable(true),
|
showSkeleton = $bindable(true),
|
||||||
styleMarginRightOverride,
|
|
||||||
isShowDeleteConfirmation = $bindable(false),
|
|
||||||
showScrollbar,
|
showScrollbar,
|
||||||
|
styleMarginRightOverride,
|
||||||
|
styleMarginContentHorizontal = '0px',
|
||||||
|
styleMarginTop = '0px',
|
||||||
|
alwaysShowScrollbar,
|
||||||
|
|
||||||
|
isShowDeleteConfirmation = $bindable(false),
|
||||||
|
|
||||||
children,
|
children,
|
||||||
skeleton,
|
skeleton,
|
||||||
empty,
|
empty,
|
||||||
header,
|
header,
|
||||||
handleTimelineScroll = () => {},
|
handleTimelineScroll = () => {},
|
||||||
|
smallHeaderHeight = {
|
||||||
|
rowHeight: 100,
|
||||||
|
headerHeight: 32,
|
||||||
|
},
|
||||||
|
largeHeaderHeight = {
|
||||||
|
rowHeight: 235,
|
||||||
|
headerHeight: 48,
|
||||||
|
},
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let { gridScrollTarget } = assetViewingStore;
|
let { gridScrollTarget } = assetViewingStore;
|
||||||
|
|
@ -70,15 +95,7 @@
|
||||||
const isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
const isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const layoutOptions = maxMd
|
const layoutOptions = maxMd ? smallHeaderHeight : largeHeaderHeight;
|
||||||
? {
|
|
||||||
rowHeight: 100,
|
|
||||||
headerHeight: 32,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
rowHeight: 235,
|
|
||||||
headerHeight: 48,
|
|
||||||
};
|
|
||||||
timelineManager.setLayoutOptions(layoutOptions);
|
timelineManager.setLayoutOptions(layoutOptions);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -173,7 +190,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<HotModuleReload
|
<HotModuleReload
|
||||||
onAfterUpdate={(payload: UpdatePayload) => {
|
onAfterUpdate={() => {
|
||||||
// when hmr happens, skeleton is initialized to true by default
|
// when hmr happens, skeleton is initialized to true by default
|
||||||
// normally, loading asset-grid is part of a navigation event, and the completion of
|
// normally, loading asset-grid is part of a navigation event, and the completion of
|
||||||
// that event triggers a scroll-to-asset, if necessary, when then clears the skeleton.
|
// that event triggers a scroll-to-asset, if necessary, when then clears the skeleton.
|
||||||
|
|
@ -186,38 +203,41 @@
|
||||||
}
|
}
|
||||||
void completeAfterNavigate({ scrollToAssetQueryParam: true });
|
void completeAfterNavigate({ scrollToAssetQueryParam: true });
|
||||||
};
|
};
|
||||||
const assetGridUpdate = payload.updates.some((update) => update.path.endsWith('Photostream.svelte'));
|
|
||||||
if (assetGridUpdate) {
|
// wait 500ms for the update to be fully swapped in
|
||||||
// wait 500ms for the update to be fully swapped in
|
setTimeout(finishHmr, 500);
|
||||||
setTimeout(finishHmr, 500);
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{@render header?.(scrollTo)}
|
{@render header?.(scrollTo)}
|
||||||
|
|
||||||
<!-- Right margin MUST be equal to the width of scrubber -->
|
<!-- Right margin MUST be equal to the width of scrubber -->
|
||||||
<section
|
<photostream
|
||||||
id="asset-grid"
|
id="asset-grid"
|
||||||
class={[
|
class={[
|
||||||
'h-full overflow-y-auto outline-none',
|
'overflow-y-auto outline-none',
|
||||||
{ 'scrollbar-hidden': !showScrollbar },
|
{ 'scrollbar-hidden': !showScrollbar },
|
||||||
|
{ 'overflow-y-scroll': alwaysShowScrollbar },
|
||||||
{ 'm-0': isEmpty },
|
{ 'm-0': isEmpty },
|
||||||
{ 'ms-0': !isEmpty },
|
{ 'ms-0': !isEmpty },
|
||||||
]}
|
]}
|
||||||
|
style:height={`calc(100% - ${styleMarginTop})`}
|
||||||
|
style:margin-top={styleMarginTop}
|
||||||
style:margin-right={styleMarginRightOverride}
|
style:margin-right={styleMarginRightOverride}
|
||||||
style:scrollbar-width={showScrollbar ? 'auto' : 'none'}
|
style:scrollbar-width={showScrollbar ? 'thin' : 'none'}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
bind:clientHeight={timelineManager.viewportHeight}
|
bind:clientHeight={timelineManager.viewportHeight}
|
||||||
bind:clientWidth={null, (v: number) => ((timelineManager.viewportWidth = v), updateSlidingWindow())}
|
|
||||||
bind:this={element}
|
bind:this={element}
|
||||||
onscroll={() => (handleTimelineScroll(), updateSlidingWindow(), updateIsScrolling())}
|
onscroll={() => (handleTimelineScroll(), updateSlidingWindow(), updateIsScrolling())}
|
||||||
>
|
>
|
||||||
<section
|
<section
|
||||||
bind:this={timelineElement}
|
bind:this={timelineElement}
|
||||||
id="virtual-timeline"
|
id="virtual-timeline"
|
||||||
|
style:margin-left={styleMarginContentHorizontal}
|
||||||
|
style:margin-right={styleMarginContentHorizontal}
|
||||||
class:invisible={showSkeleton}
|
class:invisible={showSkeleton}
|
||||||
style:height={timelineManager.timelineHeight + 'px'}
|
style:height={timelineManager.timelineHeight + 'px'}
|
||||||
|
bind:clientWidth={null, (v: number) => ((timelineManager.viewportWidth = v), updateSlidingWindow())}
|
||||||
>
|
>
|
||||||
<section
|
<section
|
||||||
use:resizeObserver={topSectionResizeObserver}
|
use:resizeObserver={topSectionResizeObserver}
|
||||||
|
|
@ -234,13 +254,15 @@
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{#each timelineManager.months as monthGroup (monthGroup.id)}
|
{#each timelineManager.months as monthGroup (monthGroup.id)}
|
||||||
{@const shouldDisplay = monthGroup.intersecting}
|
{@const shouldDisplay = monthGroup.intersecting && monthGroup.isLoaded}
|
||||||
{@const absoluteHeight = monthGroup.top}
|
{@const absoluteHeight = monthGroup.top}
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="month-group"
|
class="month-group"
|
||||||
style:height={monthGroup.height + 'px'}
|
style:margin-bottom={timelineManager.createLayoutOptions().spacing + 'px'}
|
||||||
style:position="absolute"
|
style:position="absolute"
|
||||||
style:transform={`translate3d(0,${absoluteHeight}px,0)`}
|
style:transform={`translate3d(0,${absoluteHeight}px,0)`}
|
||||||
|
style:height={`${monthGroup.height}px`}
|
||||||
style:width="100%"
|
style:width="100%"
|
||||||
>
|
>
|
||||||
{#if !shouldDisplay}
|
{#if !shouldDisplay}
|
||||||
|
|
@ -263,9 +285,12 @@
|
||||||
style:transform={`translate3d(0,${timelineManager.timelineHeight}px,0)`}
|
style:transform={`translate3d(0,${timelineManager.timelineHeight}px,0)`}
|
||||||
></div>
|
></div>
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</photostream>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
photostream {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
#asset-grid {
|
#asset-grid {
|
||||||
contain: strict;
|
contain: strict;
|
||||||
scrollbar-width: none;
|
scrollbar-width: none;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue