mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 20:25:32 -04: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