feat(web): responsive date group header height (#17944)

* feat: responsive date group header height

* update tests

* feat(web): improve perf when changing mobile orientation (#17945)

fix: improve perf when changing mobile orientation
This commit is contained in:
Min Idzelis 2025-04-29 13:59:06 -04:00 committed by GitHub
parent 07290580a6
commit 0e4cf9ac57
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 55 additions and 20 deletions

View File

@ -131,7 +131,7 @@
> >
<!-- Date group title --> <!-- Date group title -->
<div <div
class="flex z-[100] pt-[calc(1.75rem+1px)] pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm" class="flex z-[100] pt-7 pb-5 max-md:pt-5 max-md:pb-3 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm"
style:width={dateGroup.width + 'px'} style:width={dateGroup.width + 'px'}
> >
{#if !singleSelect && ((hoveredDateGroup === dateGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dateGroup.groupTitle))} {#if !singleSelect && ((hoveredDateGroup === dateGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dateGroup.groupTitle))}

View File

@ -88,7 +88,16 @@
const usingMobileDevice = $derived(mobileDevice.pointerCoarse); const usingMobileDevice = $derived(mobileDevice.pointerCoarse);
$effect(() => { $effect(() => {
assetStore.rowHeight = maxMd ? 100 : 235; const layoutOptions = maxMd
? {
rowHeight: 100,
headerHeight: 32,
}
: {
rowHeight: 235,
headerHeight: 48,
};
assetStore.setLayoutOptions(layoutOptions);
}); });
const scrollTo = (top: number) => { const scrollTo = (top: number) => {

View File

@ -9,7 +9,7 @@
<div class="overflow-clip" style:height={height + 'px'}> <div class="overflow-clip" style:height={height + 'px'}>
<div <div
class="flex z-[100] pt-[calc(1.75rem+1px)] pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm" class="flex z-[100] pt-7 pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm"
> >
{title} {title}
</div> </div>

View File

@ -48,15 +48,15 @@ describe('AssetStore', () => {
expect(plainBuckets).toEqual( expect(plainBuckets).toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ bucketDate: '2024-03-01T00:00:00.000Z', bucketHeight: 304 }), expect.objectContaining({ bucketDate: '2024-03-01T00:00:00.000Z', bucketHeight: 303 }),
expect.objectContaining({ bucketDate: '2024-02-01T00:00:00.000Z', bucketHeight: 4515.333_333_333_333 }), expect.objectContaining({ bucketDate: '2024-02-01T00:00:00.000Z', bucketHeight: 4514.333_333_333_333 }),
expect.objectContaining({ bucketDate: '2024-01-01T00:00:00.000Z', bucketHeight: 286 }), expect.objectContaining({ bucketDate: '2024-01-01T00:00:00.000Z', bucketHeight: 286 }),
]), ]),
); );
}); });
it('calculates timeline height', () => { it('calculates timeline height', () => {
expect(assetStore.timelineHeight).toBe(5105.333_333_333_333); expect(assetStore.timelineHeight).toBe(5103.333_333_333_333);
}); });
}); });

View File

@ -35,9 +35,7 @@ export type AssetStoreOptions = Omit<AssetApiGetTimeBucketsRequest, 'size'> & {
timelineAlbumId?: string; timelineAlbumId?: string;
deferInit?: boolean; deferInit?: boolean;
}; };
export type AssetStoreLayoutOptions = {
rowHeight: number;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
function updateObject(target: any, source: any): boolean { function updateObject(target: any, source: any): boolean {
if (!target) { if (!target) {
@ -110,7 +108,6 @@ export class AssetDateGroup {
readonly date: DateTime; readonly date: DateTime;
readonly dayOfMonth: number; readonly dayOfMonth: number;
intersetingAssets: IntersectingAsset[] = $state([]); intersetingAssets: IntersectingAsset[] = $state([]);
dodo: IntersectingAsset[] = $state([]);
height = $state(0); height = $state(0);
width = $state(0); width = $state(0);
@ -121,6 +118,7 @@ export class AssetDateGroup {
left: number = $state(0); left: number = $state(0);
row = $state(0); row = $state(0);
col = $state(0); col = $state(0);
deferredLayout = false;
constructor(bucket: AssetBucket, index: number, date: DateTime, dayOfMonth: number) { constructor(bucket: AssetBucket, index: number, date: DateTime, dayOfMonth: number) {
this.index = index; this.index = index;
@ -195,6 +193,10 @@ export class AssetDateGroup {
} }
layout(options: CommonLayoutOptions) { layout(options: CommonLayoutOptions) {
if (!this.bucket.intersecting) {
this.deferredLayout = true;
return;
}
const assets = this.intersetingAssets.map((intersetingAsset) => intersetingAsset.asset!); const assets = this.intersetingAssets.map((intersetingAsset) => intersetingAsset.asset!);
const geometry = getJustifiedLayoutFromAssets(assets, options); const geometry = getJustifiedLayoutFromAssets(assets, options);
this.width = geometry.containerWidth; this.width = geometry.containerWidth;
@ -547,6 +549,11 @@ export type LiteBucket = {
bucketDateFormattted: string; bucketDateFormattted: string;
}; };
type AssetStoreLayoutOptions = {
rowHeight?: number;
headerHeight?: number;
gap?: number;
};
export class AssetStore { export class AssetStore {
// --- public ---- // --- public ----
isInitialized = $state(false); isInitialized = $state(false);
@ -596,7 +603,7 @@ export class AssetStore {
#unsubscribers: Unsubscriber[] = []; #unsubscribers: Unsubscriber[] = [];
#rowHeight = $state(235); #rowHeight = $state(235);
#headerHeight = $state(49); #headerHeight = $state(48);
#gap = $state(12); #gap = $state(12);
#options: AssetStoreOptions = AssetStore.#INIT_OPTIONS; #options: AssetStoreOptions = AssetStore.#INIT_OPTIONS;
@ -608,36 +615,46 @@ export class AssetStore {
constructor() {} constructor() {}
set headerHeight(value) { setLayoutOptions({ headerHeight = 48, rowHeight = 235, gap = 12 }: AssetStoreLayoutOptions) {
let changed = false;
changed ||= this.#setHeaderHeight(headerHeight);
changed ||= this.#setGap(gap);
changed ||= this.#setRowHeight(rowHeight);
if (changed) {
this.refreshLayout();
}
}
#setHeaderHeight(value: number) {
if (this.#headerHeight == value) { if (this.#headerHeight == value) {
return; return false;
} }
this.#headerHeight = value; this.#headerHeight = value;
this.refreshLayout(); return true;
} }
get headerHeight() { get headerHeight() {
return this.#headerHeight; return this.#headerHeight;
} }
set gap(value) { #setGap(value: number) {
if (this.#gap == value) { if (this.#gap == value) {
return; return false;
} }
this.#gap = value; this.#gap = value;
this.refreshLayout(); return true;
} }
get gap() { get gap() {
return this.#gap; return this.#gap;
} }
set rowHeight(value) { #setRowHeight(value: number) {
if (this.#rowHeight == value) { if (this.#rowHeight == value) {
return; return false;
} }
this.#rowHeight = value; this.#rowHeight = value;
this.refreshLayout(); return true;
} }
get rowHeight() { get rowHeight() {
@ -815,6 +832,15 @@ export class AssetStore {
} }
bucket.intersecting = actuallyIntersecting || preIntersecting; bucket.intersecting = actuallyIntersecting || preIntersecting;
bucket.actuallyIntersecting = actuallyIntersecting; bucket.actuallyIntersecting = actuallyIntersecting;
if (preIntersecting || actuallyIntersecting) {
const hasDeferred = bucket.dateGroups.some((group) => group.deferredLayout);
if (hasDeferred) {
this.#updateGeometry(bucket, true);
for (const group of bucket.dateGroups) {
group.deferredLayout = false;
}
}
}
} }
#processPendingChanges = throttle(() => { #processPendingChanges = throttle(() => {