mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	feat(web): skeleton on asset loading (#3867)
* feat(web): skeletron on asset loading * feat: add skeleton to all asset grid views --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
		
							parent
							
								
									9539a361e4
								
							
						
					
					
						commit
						46c716d450
					
				@ -324,6 +324,22 @@
 | 
				
			|||||||
>
 | 
					>
 | 
				
			||||||
  {#if element}
 | 
					  {#if element}
 | 
				
			||||||
    <slot />
 | 
					    <slot />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- skeleton -->
 | 
				
			||||||
 | 
					    {#if !$assetStore.initialized}
 | 
				
			||||||
 | 
					      <div class="ml-[14px] mt-5">
 | 
				
			||||||
 | 
					        <div class="flex w-[120%] flex-wrap">
 | 
				
			||||||
 | 
					          {#each Array(100) as _}
 | 
				
			||||||
 | 
					            <div class="m-[1px] h-[10em] w-[16em] animate-pulse bg-immich-primary/20 dark:bg-immich-dark-primary/20" />
 | 
				
			||||||
 | 
					          {/each}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    {/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <!-- (optional) empty placeholder -->
 | 
				
			||||||
 | 
					    {#if $assetStore.initialized && $assetStore.buckets.length === 0}
 | 
				
			||||||
 | 
					      <slot name="empty" />
 | 
				
			||||||
 | 
					    {/if}
 | 
				
			||||||
    <section id="virtual-timeline" style:height={$assetStore.timelineHeight + 'px'}>
 | 
					    <section id="virtual-timeline" style:height={$assetStore.timelineHeight + 'px'}>
 | 
				
			||||||
      {#each $assetStore.buckets as bucket, bucketIndex (bucketIndex)}
 | 
					      {#each $assetStore.buckets as bucket, bucketIndex (bucketIndex)}
 | 
				
			||||||
        <IntersectionObserver
 | 
					        <IntersectionObserver
 | 
				
			||||||
 | 
				
			|||||||
@ -40,6 +40,7 @@ export class AssetStore {
 | 
				
			|||||||
  private store$ = writable(this);
 | 
					  private store$ = writable(this);
 | 
				
			||||||
  private assetToBucket: Record<string, AssetLookup> = {};
 | 
					  private assetToBucket: Record<string, AssetLookup> = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  initialized = false;
 | 
				
			||||||
  timelineHeight = 0;
 | 
					  timelineHeight = 0;
 | 
				
			||||||
  buckets: AssetBucket[] = [];
 | 
					  buckets: AssetBucket[] = [];
 | 
				
			||||||
  assets: AssetResponseDto[] = [];
 | 
					  assets: AssetResponseDto[] = [];
 | 
				
			||||||
@ -52,6 +53,7 @@ export class AssetStore {
 | 
				
			|||||||
  subscribe = this.store$.subscribe;
 | 
					  subscribe = this.store$.subscribe;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  async init(viewport: Viewport) {
 | 
					  async init(viewport: Viewport) {
 | 
				
			||||||
 | 
					    this.initialized = false;
 | 
				
			||||||
    this.timelineHeight = 0;
 | 
					    this.timelineHeight = 0;
 | 
				
			||||||
    this.buckets = [];
 | 
					    this.buckets = [];
 | 
				
			||||||
    this.assets = [];
 | 
					    this.assets = [];
 | 
				
			||||||
@ -63,6 +65,8 @@ export class AssetStore {
 | 
				
			|||||||
      key: api.getKey(),
 | 
					      key: api.getKey(),
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.initialized = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this.buckets = buckets.map((bucket) => {
 | 
					    this.buckets = buckets.map((bucket) => {
 | 
				
			||||||
      const unwrappedWidth = (3 / 2) * bucket.count * THUMBNAIL_HEIGHT * (7 / 10);
 | 
					      const unwrappedWidth = (3 / 2) * bucket.count * THUMBNAIL_HEIGHT * (7 / 10);
 | 
				
			||||||
      const rows = Math.ceil(unwrappedWidth / viewport.width);
 | 
					      const rows = Math.ceil(unwrappedWidth / viewport.width);
 | 
				
			||||||
 | 
				
			|||||||
@ -14,25 +14,18 @@
 | 
				
			|||||||
  import { AssetAction } from '$lib/constants';
 | 
					  import { AssetAction } from '$lib/constants';
 | 
				
			||||||
  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
 | 
					  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
 | 
				
			||||||
  import { AssetStore } from '$lib/stores/assets.store';
 | 
					  import { AssetStore } from '$lib/stores/assets.store';
 | 
				
			||||||
  import { api, TimeBucketSize } from '@api';
 | 
					  import { TimeBucketSize } from '@api';
 | 
				
			||||||
  import { onMount } from 'svelte';
 | 
					 | 
				
			||||||
  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
 | 
					  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
 | 
				
			||||||
  import Plus from 'svelte-material-icons/Plus.svelte';
 | 
					  import Plus from 'svelte-material-icons/Plus.svelte';
 | 
				
			||||||
  import type { PageData } from './$types';
 | 
					  import type { PageData } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export let data: PageData;
 | 
					  export let data: PageData;
 | 
				
			||||||
  let assetCount = 1;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const assetStore = new AssetStore({ size: TimeBucketSize.Month, isArchived: true });
 | 
					  const assetStore = new AssetStore({ size: TimeBucketSize.Month, isArchived: true });
 | 
				
			||||||
  const assetInteractionStore = createAssetInteractionStore();
 | 
					  const assetInteractionStore = createAssetInteractionStore();
 | 
				
			||||||
  const { isMultiSelectState, selectedAssets } = assetInteractionStore;
 | 
					  const { isMultiSelectState, selectedAssets } = assetInteractionStore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
 | 
					  $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
 | 
				
			||||||
 | 
					 | 
				
			||||||
  onMount(async () => {
 | 
					 | 
				
			||||||
    const { data: stats } = await api.assetApi.getAssetStats({ isArchived: true });
 | 
					 | 
				
			||||||
    assetCount = stats.total;
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
{#if $isMultiSelectState}
 | 
					{#if $isMultiSelectState}
 | 
				
			||||||
@ -52,10 +45,12 @@
 | 
				
			|||||||
  </AssetSelectControlBar>
 | 
					  </AssetSelectControlBar>
 | 
				
			||||||
{/if}
 | 
					{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={!assetCount}>
 | 
					<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title}>
 | 
				
			||||||
  {#if assetCount}
 | 
					  <AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.UNARCHIVE}>
 | 
				
			||||||
    <AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.UNARCHIVE} />
 | 
					    <EmptyPlaceholder
 | 
				
			||||||
  {:else}
 | 
					      text="Archive photos and videos to hide them from your Photos view"
 | 
				
			||||||
    <EmptyPlaceholder text="Archive photos and videos to hide them from your Photos view" alt="Empty archive" />
 | 
					      alt="Empty archive"
 | 
				
			||||||
  {/if}
 | 
					      slot="empty"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  </AssetGrid>
 | 
				
			||||||
</UserPageLayout>
 | 
					</UserPageLayout>
 | 
				
			||||||
 | 
				
			|||||||
@ -14,25 +14,18 @@
 | 
				
			|||||||
  import { AssetAction } from '$lib/constants';
 | 
					  import { AssetAction } from '$lib/constants';
 | 
				
			||||||
  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
 | 
					  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
 | 
				
			||||||
  import { AssetStore } from '$lib/stores/assets.store';
 | 
					  import { AssetStore } from '$lib/stores/assets.store';
 | 
				
			||||||
  import { api, TimeBucketSize } from '@api';
 | 
					  import { TimeBucketSize } from '@api';
 | 
				
			||||||
  import { onMount } from 'svelte';
 | 
					 | 
				
			||||||
  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
 | 
					  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
 | 
				
			||||||
  import Plus from 'svelte-material-icons/Plus.svelte';
 | 
					  import Plus from 'svelte-material-icons/Plus.svelte';
 | 
				
			||||||
  import type { PageData } from './$types';
 | 
					  import type { PageData } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export let data: PageData;
 | 
					  export let data: PageData;
 | 
				
			||||||
  let assetCount = 1;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const assetStore = new AssetStore({ size: TimeBucketSize.Month, isFavorite: true });
 | 
					  const assetStore = new AssetStore({ size: TimeBucketSize.Month, isFavorite: true });
 | 
				
			||||||
  const assetInteractionStore = createAssetInteractionStore();
 | 
					  const assetInteractionStore = createAssetInteractionStore();
 | 
				
			||||||
  const { isMultiSelectState, selectedAssets } = assetInteractionStore;
 | 
					  const { isMultiSelectState, selectedAssets } = assetInteractionStore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $: isAllArchive = Array.from($selectedAssets).every((asset) => asset.isArchived);
 | 
					  $: isAllArchive = Array.from($selectedAssets).every((asset) => asset.isArchived);
 | 
				
			||||||
 | 
					 | 
				
			||||||
  onMount(async () => {
 | 
					 | 
				
			||||||
    const { data: stats } = await api.assetApi.getAssetStats({ isFavorite: true });
 | 
					 | 
				
			||||||
    assetCount = stats.total;
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<!-- Multiselection mode app bar -->
 | 
					<!-- Multiselection mode app bar -->
 | 
				
			||||||
@ -53,10 +46,12 @@
 | 
				
			|||||||
  </AssetSelectControlBar>
 | 
					  </AssetSelectControlBar>
 | 
				
			||||||
{/if}
 | 
					{/if}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={!assetCount}>
 | 
					<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title}>
 | 
				
			||||||
  {#if assetCount}
 | 
					  <AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.UNFAVORITE}>
 | 
				
			||||||
    <AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.UNFAVORITE} />
 | 
					    <EmptyPlaceholder
 | 
				
			||||||
  {:else}
 | 
					      text="Add favorites to quickly find your best pictures and videos"
 | 
				
			||||||
    <EmptyPlaceholder text="Add favorites to quickly find your best pictures and videos" alt="Empty favorites" />
 | 
					      alt="Empty favorites"
 | 
				
			||||||
  {/if}
 | 
					      slot="empty"
 | 
				
			||||||
 | 
					    />
 | 
				
			||||||
 | 
					  </AssetGrid>
 | 
				
			||||||
</UserPageLayout>
 | 
					</UserPageLayout>
 | 
				
			||||||
 | 
				
			|||||||
@ -17,25 +17,18 @@
 | 
				
			|||||||
  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
 | 
					  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
 | 
				
			||||||
  import { AssetStore } from '$lib/stores/assets.store';
 | 
					  import { AssetStore } from '$lib/stores/assets.store';
 | 
				
			||||||
  import { openFileUploadDialog } from '$lib/utils/file-uploader';
 | 
					  import { openFileUploadDialog } from '$lib/utils/file-uploader';
 | 
				
			||||||
  import { TimeBucketSize, api } from '@api';
 | 
					  import { TimeBucketSize } from '@api';
 | 
				
			||||||
  import { onMount } from 'svelte';
 | 
					 | 
				
			||||||
  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
 | 
					  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
 | 
				
			||||||
  import Plus from 'svelte-material-icons/Plus.svelte';
 | 
					  import Plus from 'svelte-material-icons/Plus.svelte';
 | 
				
			||||||
  import type { PageData } from './$types';
 | 
					  import type { PageData } from './$types';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  export let data: PageData;
 | 
					  export let data: PageData;
 | 
				
			||||||
  let assetCount = 1;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const assetStore = new AssetStore({ size: TimeBucketSize.Month, isArchived: false });
 | 
					  const assetStore = new AssetStore({ size: TimeBucketSize.Month, isArchived: false });
 | 
				
			||||||
  const assetInteractionStore = createAssetInteractionStore();
 | 
					  const assetInteractionStore = createAssetInteractionStore();
 | 
				
			||||||
  const { isMultiSelectState, selectedAssets } = assetInteractionStore;
 | 
					  const { isMultiSelectState, selectedAssets } = assetInteractionStore;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
 | 
					  $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
 | 
				
			||||||
 | 
					 | 
				
			||||||
  onMount(async () => {
 | 
					 | 
				
			||||||
    const { data: stats } = await api.assetApi.getAssetStats({ isArchived: false });
 | 
					 | 
				
			||||||
    assetCount = stats.total;
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} showUploadButton>
 | 
					<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} showUploadButton>
 | 
				
			||||||
@ -59,14 +52,15 @@
 | 
				
			|||||||
    {/if}
 | 
					    {/if}
 | 
				
			||||||
  </svelte:fragment>
 | 
					  </svelte:fragment>
 | 
				
			||||||
  <svelte:fragment slot="content">
 | 
					  <svelte:fragment slot="content">
 | 
				
			||||||
    {#if assetCount}
 | 
					    <AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.ARCHIVE}>
 | 
				
			||||||
      <AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.ARCHIVE}>
 | 
					      {#if data.user.memoriesEnabled}
 | 
				
			||||||
        {#if data.user.memoriesEnabled}
 | 
					        <MemoryLane />
 | 
				
			||||||
          <MemoryLane />
 | 
					      {/if}
 | 
				
			||||||
        {/if}
 | 
					      <EmptyPlaceholder
 | 
				
			||||||
      </AssetGrid>
 | 
					        text="CLICK TO UPLOAD YOUR FIRST PHOTO"
 | 
				
			||||||
    {:else}
 | 
					        actionHandler={() => openFileUploadDialog()}
 | 
				
			||||||
      <EmptyPlaceholder text="CLICK TO UPLOAD YOUR FIRST PHOTO" actionHandler={() => openFileUploadDialog()} />
 | 
					        slot="empty"
 | 
				
			||||||
    {/if}
 | 
					      />
 | 
				
			||||||
 | 
					    </AssetGrid>
 | 
				
			||||||
  </svelte:fragment>
 | 
					  </svelte:fragment>
 | 
				
			||||||
</UserPageLayout>
 | 
					</UserPageLayout>
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user