ソースを参照

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>
JasBogans 1 年間 前
コミット
46c716d450

+ 16 - 0
web/src/lib/components/photos-page/asset-grid.svelte

@@ -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

+ 4 - 0
web/src/lib/stores/assets.store.ts

@@ -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);

+ 9 - 14
web/src/routes/(user)/archive/+page.svelte

@@ -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>

+ 9 - 14
web/src/routes/(user)/favorites/+page.svelte

@@ -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>

+ 11 - 17
web/src/routes/(user)/photos/+page.svelte

@@ -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}
-      </AssetGrid>
-    {:else}
-      <EmptyPlaceholder text="CLICK TO UPLOAD YOUR FIRST PHOTO" actionHandler={() => openFileUploadDialog()} />
-    {/if}
+    <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>
   </svelte:fragment>
 </UserPageLayout>