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}
 | 
			
		||||
    <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'}>
 | 
			
		||||
      {#each $assetStore.buckets as bucket, bucketIndex (bucketIndex)}
 | 
			
		||||
        <IntersectionObserver
 | 
			
		||||
 | 
			
		||||
@ -40,6 +40,7 @@ export class AssetStore {
 | 
			
		||||
  private store$ = writable(this);
 | 
			
		||||
  private assetToBucket: Record<string, AssetLookup> = {};
 | 
			
		||||
 | 
			
		||||
  initialized = false;
 | 
			
		||||
  timelineHeight = 0;
 | 
			
		||||
  buckets: AssetBucket[] = [];
 | 
			
		||||
  assets: AssetResponseDto[] = [];
 | 
			
		||||
@ -52,6 +53,7 @@ export class AssetStore {
 | 
			
		||||
  subscribe = this.store$.subscribe;
 | 
			
		||||
 | 
			
		||||
  async init(viewport: Viewport) {
 | 
			
		||||
    this.initialized = false;
 | 
			
		||||
    this.timelineHeight = 0;
 | 
			
		||||
    this.buckets = [];
 | 
			
		||||
    this.assets = [];
 | 
			
		||||
@ -63,6 +65,8 @@ export class AssetStore {
 | 
			
		||||
      key: api.getKey(),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    this.initialized = true;
 | 
			
		||||
 | 
			
		||||
    this.buckets = buckets.map((bucket) => {
 | 
			
		||||
      const unwrappedWidth = (3 / 2) * bucket.count * THUMBNAIL_HEIGHT * (7 / 10);
 | 
			
		||||
      const rows = Math.ceil(unwrappedWidth / viewport.width);
 | 
			
		||||
 | 
			
		||||
@ -14,25 +14,18 @@
 | 
			
		||||
  import { AssetAction } from '$lib/constants';
 | 
			
		||||
  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
 | 
			
		||||
  import { AssetStore } from '$lib/stores/assets.store';
 | 
			
		||||
  import { api, TimeBucketSize } from '@api';
 | 
			
		||||
  import { onMount } from 'svelte';
 | 
			
		||||
  import { TimeBucketSize } from '@api';
 | 
			
		||||
  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
 | 
			
		||||
  import Plus from 'svelte-material-icons/Plus.svelte';
 | 
			
		||||
  import type { PageData } from './$types';
 | 
			
		||||
 | 
			
		||||
  export let data: PageData;
 | 
			
		||||
  let assetCount = 1;
 | 
			
		||||
 | 
			
		||||
  const assetStore = new AssetStore({ size: TimeBucketSize.Month, isArchived: true });
 | 
			
		||||
  const assetInteractionStore = createAssetInteractionStore();
 | 
			
		||||
  const { isMultiSelectState, selectedAssets } = assetInteractionStore;
 | 
			
		||||
 | 
			
		||||
  $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
 | 
			
		||||
 | 
			
		||||
  onMount(async () => {
 | 
			
		||||
    const { data: stats } = await api.assetApi.getAssetStats({ isArchived: true });
 | 
			
		||||
    assetCount = stats.total;
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
{#if $isMultiSelectState}
 | 
			
		||||
@ -52,10 +45,12 @@
 | 
			
		||||
  </AssetSelectControlBar>
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={!assetCount}>
 | 
			
		||||
  {#if assetCount}
 | 
			
		||||
    <AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.UNARCHIVE} />
 | 
			
		||||
  {:else}
 | 
			
		||||
    <EmptyPlaceholder text="Archive photos and videos to hide them from your Photos view" alt="Empty archive" />
 | 
			
		||||
  {/if}
 | 
			
		||||
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title}>
 | 
			
		||||
  <AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.UNARCHIVE}>
 | 
			
		||||
    <EmptyPlaceholder
 | 
			
		||||
      text="Archive photos and videos to hide them from your Photos view"
 | 
			
		||||
      alt="Empty archive"
 | 
			
		||||
      slot="empty"
 | 
			
		||||
    />
 | 
			
		||||
  </AssetGrid>
 | 
			
		||||
</UserPageLayout>
 | 
			
		||||
 | 
			
		||||
@ -14,25 +14,18 @@
 | 
			
		||||
  import { AssetAction } from '$lib/constants';
 | 
			
		||||
  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
 | 
			
		||||
  import { AssetStore } from '$lib/stores/assets.store';
 | 
			
		||||
  import { api, TimeBucketSize } from '@api';
 | 
			
		||||
  import { onMount } from 'svelte';
 | 
			
		||||
  import { TimeBucketSize } from '@api';
 | 
			
		||||
  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
 | 
			
		||||
  import Plus from 'svelte-material-icons/Plus.svelte';
 | 
			
		||||
  import type { PageData } from './$types';
 | 
			
		||||
 | 
			
		||||
  export let data: PageData;
 | 
			
		||||
  let assetCount = 1;
 | 
			
		||||
 | 
			
		||||
  const assetStore = new AssetStore({ size: TimeBucketSize.Month, isFavorite: true });
 | 
			
		||||
  const assetInteractionStore = createAssetInteractionStore();
 | 
			
		||||
  const { isMultiSelectState, selectedAssets } = assetInteractionStore;
 | 
			
		||||
 | 
			
		||||
  $: isAllArchive = Array.from($selectedAssets).every((asset) => asset.isArchived);
 | 
			
		||||
 | 
			
		||||
  onMount(async () => {
 | 
			
		||||
    const { data: stats } = await api.assetApi.getAssetStats({ isFavorite: true });
 | 
			
		||||
    assetCount = stats.total;
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<!-- Multiselection mode app bar -->
 | 
			
		||||
@ -53,10 +46,12 @@
 | 
			
		||||
  </AssetSelectControlBar>
 | 
			
		||||
{/if}
 | 
			
		||||
 | 
			
		||||
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title} scrollbar={!assetCount}>
 | 
			
		||||
  {#if assetCount}
 | 
			
		||||
    <AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.UNFAVORITE} />
 | 
			
		||||
  {:else}
 | 
			
		||||
    <EmptyPlaceholder text="Add favorites to quickly find your best pictures and videos" alt="Empty favorites" />
 | 
			
		||||
  {/if}
 | 
			
		||||
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} title={data.meta.title}>
 | 
			
		||||
  <AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.UNFAVORITE}>
 | 
			
		||||
    <EmptyPlaceholder
 | 
			
		||||
      text="Add favorites to quickly find your best pictures and videos"
 | 
			
		||||
      alt="Empty favorites"
 | 
			
		||||
      slot="empty"
 | 
			
		||||
    />
 | 
			
		||||
  </AssetGrid>
 | 
			
		||||
</UserPageLayout>
 | 
			
		||||
 | 
			
		||||
@ -17,25 +17,18 @@
 | 
			
		||||
  import { createAssetInteractionStore } from '$lib/stores/asset-interaction.store';
 | 
			
		||||
  import { AssetStore } from '$lib/stores/assets.store';
 | 
			
		||||
  import { openFileUploadDialog } from '$lib/utils/file-uploader';
 | 
			
		||||
  import { TimeBucketSize, api } from '@api';
 | 
			
		||||
  import { onMount } from 'svelte';
 | 
			
		||||
  import { TimeBucketSize } from '@api';
 | 
			
		||||
  import DotsVertical from 'svelte-material-icons/DotsVertical.svelte';
 | 
			
		||||
  import Plus from 'svelte-material-icons/Plus.svelte';
 | 
			
		||||
  import type { PageData } from './$types';
 | 
			
		||||
 | 
			
		||||
  export let data: PageData;
 | 
			
		||||
  let assetCount = 1;
 | 
			
		||||
 | 
			
		||||
  const assetStore = new AssetStore({ size: TimeBucketSize.Month, isArchived: false });
 | 
			
		||||
  const assetInteractionStore = createAssetInteractionStore();
 | 
			
		||||
  const { isMultiSelectState, selectedAssets } = assetInteractionStore;
 | 
			
		||||
 | 
			
		||||
  $: isAllFavorite = Array.from($selectedAssets).every((asset) => asset.isFavorite);
 | 
			
		||||
 | 
			
		||||
  onMount(async () => {
 | 
			
		||||
    const { data: stats } = await api.assetApi.getAssetStats({ isArchived: false });
 | 
			
		||||
    assetCount = stats.total;
 | 
			
		||||
  });
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<UserPageLayout user={data.user} hideNavbar={$isMultiSelectState} showUploadButton>
 | 
			
		||||
@ -59,14 +52,15 @@
 | 
			
		||||
    {/if}
 | 
			
		||||
  </svelte:fragment>
 | 
			
		||||
  <svelte:fragment slot="content">
 | 
			
		||||
    {#if assetCount}
 | 
			
		||||
    <AssetGrid {assetStore} {assetInteractionStore} removeAction={AssetAction.ARCHIVE}>
 | 
			
		||||
      {#if data.user.memoriesEnabled}
 | 
			
		||||
        <MemoryLane />
 | 
			
		||||
      {/if}
 | 
			
		||||
      <EmptyPlaceholder
 | 
			
		||||
        text="CLICK TO UPLOAD YOUR FIRST PHOTO"
 | 
			
		||||
        actionHandler={() => openFileUploadDialog()}
 | 
			
		||||
        slot="empty"
 | 
			
		||||
      />
 | 
			
		||||
    </AssetGrid>
 | 
			
		||||
    {:else}
 | 
			
		||||
      <EmptyPlaceholder text="CLICK TO UPLOAD YOUR FIRST PHOTO" actionHandler={() => openFileUploadDialog()} />
 | 
			
		||||
    {/if}
 | 
			
		||||
  </svelte:fragment>
 | 
			
		||||
</UserPageLayout>
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user