mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	feat(web): dedicated view for user's usage stats (#14348)
* feat(web): dedicated view for user's usage stats * cell heights * Translation * pr feedback * clean up * clean up * pr feedback
This commit is contained in:
		
							parent
							
								
									d277096d58
								
							
						
					
					
						commit
						361d83c729
					
				@ -1,4 +1,6 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
 | 
					  "user_usage_stats": "Account usage statistics",
 | 
				
			||||||
 | 
					  "user_usage_stats_description": "View account usage statistics",
 | 
				
			||||||
  "about": "Refresh",
 | 
					  "about": "Refresh",
 | 
				
			||||||
  "account": "Account",
 | 
					  "account": "Account",
 | 
				
			||||||
  "account_settings": "Account Settings",
 | 
					  "account_settings": "Account Settings",
 | 
				
			||||||
@ -1315,5 +1317,7 @@
 | 
				
			|||||||
  "years_ago": "{years, plural, one {# year} other {# years}} ago",
 | 
					  "years_ago": "{years, plural, one {# year} other {# years}} ago",
 | 
				
			||||||
  "yes": "Yes",
 | 
					  "yes": "Yes",
 | 
				
			||||||
  "you_dont_have_any_shared_links": "You don't have any shared links",
 | 
					  "you_dont_have_any_shared_links": "You don't have any shared links",
 | 
				
			||||||
  "zoom_image": "Zoom Image"
 | 
					  "zoom_image": "Zoom Image",
 | 
				
			||||||
 | 
					  "timeline": "Timeline",
 | 
				
			||||||
 | 
					  "total": "Total"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -1,27 +0,0 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					 | 
				
			||||||
  import { type AlbumStatisticsResponseDto, getAlbumStatistics } from '@immich/sdk';
 | 
					 | 
				
			||||||
  import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
 | 
					 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  interface Props {
 | 
					 | 
				
			||||||
    albumType: keyof AlbumStatisticsResponseDto;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let { albumType }: Props = $props();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const handleAlbumCount = async () => {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      return await getAlbumStatistics();
 | 
					 | 
				
			||||||
    } catch {
 | 
					 | 
				
			||||||
      return { owned: 0, shared: 0, notShared: 0 };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{#await handleAlbumCount()}
 | 
					 | 
				
			||||||
  <LoadingSpinner />
 | 
					 | 
				
			||||||
{:then data}
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <p>{$t('albums_count', { values: { count: data[albumType] } })}</p>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
{/await}
 | 
					 | 
				
			||||||
@ -1,20 +0,0 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					 | 
				
			||||||
  import { getAssetStatistics } from '@immich/sdk';
 | 
					 | 
				
			||||||
  import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
 | 
					 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  interface Props {
 | 
					 | 
				
			||||||
    assetStats: NonNullable<Parameters<typeof getAssetStatistics>[0]>;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let { assetStats }: Props = $props();
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
{#await getAssetStatistics(assetStats)}
 | 
					 | 
				
			||||||
  <LoadingSpinner />
 | 
					 | 
				
			||||||
{:then data}
 | 
					 | 
				
			||||||
  <div>
 | 
					 | 
				
			||||||
    <p>{$t('videos_count', { values: { count: data.videos } })}</p>
 | 
					 | 
				
			||||||
    <p>{$t('photos_count', { values: { count: data.images } })}</p>
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
{/await}
 | 
					 | 
				
			||||||
@ -1,10 +1,7 @@
 | 
				
			|||||||
<script lang="ts">
 | 
					<script lang="ts">
 | 
				
			||||||
  import { fade } from 'svelte/transition';
 | 
					 | 
				
			||||||
  import Icon from '$lib/components/elements/icon.svelte';
 | 
					  import Icon from '$lib/components/elements/icon.svelte';
 | 
				
			||||||
  import { mdiInformationOutline } from '@mdi/js';
 | 
					 | 
				
			||||||
  import { resolveRoute } from '$app/paths';
 | 
					  import { resolveRoute } from '$app/paths';
 | 
				
			||||||
  import { page } from '$app/stores';
 | 
					  import { page } from '$app/stores';
 | 
				
			||||||
  import type { Snippet } from 'svelte';
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    title: string;
 | 
					    title: string;
 | 
				
			||||||
@ -13,7 +10,6 @@
 | 
				
			|||||||
    flippedLogo?: boolean;
 | 
					    flippedLogo?: boolean;
 | 
				
			||||||
    isSelected?: boolean;
 | 
					    isSelected?: boolean;
 | 
				
			||||||
    preloadData?: boolean;
 | 
					    preloadData?: boolean;
 | 
				
			||||||
    moreInformation?: Snippet;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let {
 | 
					  let {
 | 
				
			||||||
@ -23,10 +19,8 @@
 | 
				
			|||||||
    flippedLogo = false,
 | 
					    flippedLogo = false,
 | 
				
			||||||
    isSelected = $bindable(false),
 | 
					    isSelected = $bindable(false),
 | 
				
			||||||
    preloadData = true,
 | 
					    preloadData = true,
 | 
				
			||||||
    moreInformation,
 | 
					 | 
				
			||||||
  }: Props = $props();
 | 
					  }: Props = $props();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  let showMoreInformation = $state(false);
 | 
					 | 
				
			||||||
  let routePath = $derived(resolveRoute(routeId, {}));
 | 
					  let routePath = $derived(resolveRoute(routeId, {}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  $effect(() => {
 | 
					  $effect(() => {
 | 
				
			||||||
@ -39,7 +33,7 @@
 | 
				
			|||||||
  data-sveltekit-preload-data={preloadData ? 'hover' : 'off'}
 | 
					  data-sveltekit-preload-data={preloadData ? 'hover' : 'off'}
 | 
				
			||||||
  draggable="false"
 | 
					  draggable="false"
 | 
				
			||||||
  aria-current={isSelected ? 'page' : undefined}
 | 
					  aria-current={isSelected ? 'page' : undefined}
 | 
				
			||||||
  class="flex w-full place-items-center justify-between gap-4 rounded-r-full py-3 transition-[padding] delay-100 duration-100 hover:cursor-pointer hover:bg-immich-gray hover:text-immich-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary
 | 
					  class="flex w-full place-items-center gap-4 rounded-r-full py-3 transition-[padding] delay-100 duration-100 hover:cursor-pointer hover:bg-immich-gray hover:text-immich-primary dark:text-immich-dark-fg dark:hover:bg-immich-dark-gray dark:hover:text-immich-dark-primary
 | 
				
			||||||
    {isSelected
 | 
					    {isSelected
 | 
				
			||||||
    ? 'bg-immich-primary/10 text-immich-primary hover:bg-immich-primary/10 dark:bg-immich-dark-primary/10 dark:text-immich-dark-primary'
 | 
					    ? 'bg-immich-primary/10 text-immich-primary hover:bg-immich-primary/10 dark:bg-immich-dark-primary/10 dark:text-immich-dark-primary'
 | 
				
			||||||
    : ''}
 | 
					    : ''}
 | 
				
			||||||
@ -50,33 +44,5 @@
 | 
				
			|||||||
    <Icon path={icon} size="1.5em" class="shrink-0" flipped={flippedLogo} ariaHidden />
 | 
					    <Icon path={icon} size="1.5em" class="shrink-0" flipped={flippedLogo} ariaHidden />
 | 
				
			||||||
    <span class="text-sm font-medium">{title}</span>
 | 
					    <span class="text-sm font-medium">{title}</span>
 | 
				
			||||||
  </div>
 | 
					  </div>
 | 
				
			||||||
 | 
					  <div></div>
 | 
				
			||||||
  <div
 | 
					 | 
				
			||||||
    class="h-0 overflow-hidden transition-[height] delay-1000 duration-100 sm:group-hover:h-auto group-hover:sm:overflow-visible md:h-auto md:overflow-visible"
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    {#if moreInformation}
 | 
					 | 
				
			||||||
      <!-- svelte-ignore a11y_no_static_element_interactions -->
 | 
					 | 
				
			||||||
      <div
 | 
					 | 
				
			||||||
        class="relative flex cursor-default select-none justify-center"
 | 
					 | 
				
			||||||
        onmouseenter={() => (showMoreInformation = true)}
 | 
					 | 
				
			||||||
        onmouseleave={() => (showMoreInformation = false)}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <div class="p-1 text-gray-600 hover:cursor-help dark:text-gray-400">
 | 
					 | 
				
			||||||
          <Icon path={mdiInformationOutline} />
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        {#if showMoreInformation}
 | 
					 | 
				
			||||||
          <div class="absolute right-6 top-0">
 | 
					 | 
				
			||||||
            <div
 | 
					 | 
				
			||||||
              class="flex place-content-center place-items-center whitespace-nowrap rounded-3xl border bg-immich-bg px-6 py-3 text-xs text-immich-fg shadow-lg dark:border-immich-dark-gray dark:bg-gray-600 dark:text-immich-dark-fg"
 | 
					 | 
				
			||||||
              class:hidden={!showMoreInformation}
 | 
					 | 
				
			||||||
              transition:fade={{ duration: 200 }}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              {@render moreInformation?.()}
 | 
					 | 
				
			||||||
            </div>
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        {/if}
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    {/if}
 | 
					 | 
				
			||||||
  </div>
 | 
					 | 
				
			||||||
</a>
 | 
					</a>
 | 
				
			||||||
 | 
				
			|||||||
@ -24,8 +24,6 @@
 | 
				
			|||||||
  } from '@mdi/js';
 | 
					  } from '@mdi/js';
 | 
				
			||||||
  import SideBarSection from './side-bar-section.svelte';
 | 
					  import SideBarSection from './side-bar-section.svelte';
 | 
				
			||||||
  import SideBarLink from './side-bar-link.svelte';
 | 
					  import SideBarLink from './side-bar-link.svelte';
 | 
				
			||||||
  import MoreInformationAssets from '$lib/components/shared-components/side-bar/more-information-assets.svelte';
 | 
					 | 
				
			||||||
  import MoreInformationAlbums from '$lib/components/shared-components/side-bar/more-information-albums.svelte';
 | 
					 | 
				
			||||||
  import { t } from 'svelte-i18n';
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
  import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
 | 
					  import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
 | 
				
			||||||
  import { preferences } from '$lib/stores/user.store';
 | 
					  import { preferences } from '$lib/stores/user.store';
 | 
				
			||||||
@ -47,11 +45,7 @@
 | 
				
			|||||||
      routeId="/(user)/photos"
 | 
					      routeId="/(user)/photos"
 | 
				
			||||||
      bind:isSelected={isPhotosSelected}
 | 
					      bind:isSelected={isPhotosSelected}
 | 
				
			||||||
      icon={isPhotosSelected ? mdiImageMultiple : mdiImageMultipleOutline}
 | 
					      icon={isPhotosSelected ? mdiImageMultiple : mdiImageMultipleOutline}
 | 
				
			||||||
    >
 | 
					    ></SideBarLink>
 | 
				
			||||||
      {#snippet moreInformation()}
 | 
					 | 
				
			||||||
        <MoreInformationAssets assetStats={{ isArchived: false }} />
 | 
					 | 
				
			||||||
      {/snippet}
 | 
					 | 
				
			||||||
    </SideBarLink>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {#if $featureFlags.search}
 | 
					    {#if $featureFlags.search}
 | 
				
			||||||
      <SideBarLink title={$t('explore')} routeId="/(user)/explore" icon={mdiMagnify} />
 | 
					      <SideBarLink title={$t('explore')} routeId="/(user)/explore" icon={mdiMagnify} />
 | 
				
			||||||
@ -80,11 +74,7 @@
 | 
				
			|||||||
      routeId="/(user)/sharing"
 | 
					      routeId="/(user)/sharing"
 | 
				
			||||||
      icon={isSharingSelected ? mdiAccountMultiple : mdiAccountMultipleOutline}
 | 
					      icon={isSharingSelected ? mdiAccountMultiple : mdiAccountMultipleOutline}
 | 
				
			||||||
      bind:isSelected={isSharingSelected}
 | 
					      bind:isSelected={isSharingSelected}
 | 
				
			||||||
    >
 | 
					    ></SideBarLink>
 | 
				
			||||||
      {#snippet moreInformation()}
 | 
					 | 
				
			||||||
        <MoreInformationAlbums albumType="shared" />
 | 
					 | 
				
			||||||
      {/snippet}
 | 
					 | 
				
			||||||
    </SideBarLink>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <div class="text-xs transition-all duration-200 dark:text-immich-dark-fg">
 | 
					    <div class="text-xs transition-all duration-200 dark:text-immich-dark-fg">
 | 
				
			||||||
      <p class="hidden p-6 group-hover:sm:block md:block">{$t('library').toUpperCase()}</p>
 | 
					      <p class="hidden p-6 group-hover:sm:block md:block">{$t('library').toUpperCase()}</p>
 | 
				
			||||||
@ -96,17 +86,9 @@
 | 
				
			|||||||
      routeId="/(user)/favorites"
 | 
					      routeId="/(user)/favorites"
 | 
				
			||||||
      icon={isFavoritesSelected ? mdiHeart : mdiHeartOutline}
 | 
					      icon={isFavoritesSelected ? mdiHeart : mdiHeartOutline}
 | 
				
			||||||
      bind:isSelected={isFavoritesSelected}
 | 
					      bind:isSelected={isFavoritesSelected}
 | 
				
			||||||
    >
 | 
					    ></SideBarLink>
 | 
				
			||||||
      {#snippet moreInformation()}
 | 
					 | 
				
			||||||
        <MoreInformationAssets assetStats={{ isFavorite: true }} />
 | 
					 | 
				
			||||||
      {/snippet}
 | 
					 | 
				
			||||||
    </SideBarLink>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <SideBarLink title={$t('albums')} routeId="/(user)/albums" icon={mdiImageAlbum} flippedLogo>
 | 
					    <SideBarLink title={$t('albums')} routeId="/(user)/albums" icon={mdiImageAlbum} flippedLogo></SideBarLink>
 | 
				
			||||||
      {#snippet moreInformation()}
 | 
					 | 
				
			||||||
        <MoreInformationAlbums albumType="owned" />
 | 
					 | 
				
			||||||
      {/snippet}
 | 
					 | 
				
			||||||
    </SideBarLink>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {#if $preferences.tags.enabled && $preferences.tags.sidebarWeb}
 | 
					    {#if $preferences.tags.enabled && $preferences.tags.sidebarWeb}
 | 
				
			||||||
      <SideBarLink title={$t('tags')} routeId="/(user)/tags" icon={mdiTagMultipleOutline} flippedLogo />
 | 
					      <SideBarLink title={$t('tags')} routeId="/(user)/tags" icon={mdiTagMultipleOutline} flippedLogo />
 | 
				
			||||||
@ -128,11 +110,7 @@
 | 
				
			|||||||
      routeId="/(user)/archive"
 | 
					      routeId="/(user)/archive"
 | 
				
			||||||
      bind:isSelected={isArchiveSelected}
 | 
					      bind:isSelected={isArchiveSelected}
 | 
				
			||||||
      icon={isArchiveSelected ? mdiArchiveArrowDown : mdiArchiveArrowDownOutline}
 | 
					      icon={isArchiveSelected ? mdiArchiveArrowDown : mdiArchiveArrowDownOutline}
 | 
				
			||||||
    >
 | 
					    ></SideBarLink>
 | 
				
			||||||
      {#snippet moreInformation()}
 | 
					 | 
				
			||||||
        <MoreInformationAssets assetStats={{ isArchived: true }} />
 | 
					 | 
				
			||||||
      {/snippet}
 | 
					 | 
				
			||||||
    </SideBarLink>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    {#if $featureFlags.trash}
 | 
					    {#if $featureFlags.trash}
 | 
				
			||||||
      <SideBarLink
 | 
					      <SideBarLink
 | 
				
			||||||
@ -140,11 +118,7 @@
 | 
				
			|||||||
        routeId="/(user)/trash"
 | 
					        routeId="/(user)/trash"
 | 
				
			||||||
        bind:isSelected={isTrashSelected}
 | 
					        bind:isSelected={isTrashSelected}
 | 
				
			||||||
        icon={isTrashSelected ? mdiTrashCan : mdiTrashCanOutline}
 | 
					        icon={isTrashSelected ? mdiTrashCan : mdiTrashCanOutline}
 | 
				
			||||||
      >
 | 
					      ></SideBarLink>
 | 
				
			||||||
        {#snippet moreInformation()}
 | 
					 | 
				
			||||||
          <MoreInformationAssets assetStats={{ isTrashed: true }} />
 | 
					 | 
				
			||||||
        {/snippet}
 | 
					 | 
				
			||||||
      </SideBarLink>
 | 
					 | 
				
			||||||
    {/if}
 | 
					    {/if}
 | 
				
			||||||
  </nav>
 | 
					  </nav>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -30,8 +30,10 @@
 | 
				
			|||||||
    mdiFeatureSearchOutline,
 | 
					    mdiFeatureSearchOutline,
 | 
				
			||||||
    mdiKeyOutline,
 | 
					    mdiKeyOutline,
 | 
				
			||||||
    mdiOnepassword,
 | 
					    mdiOnepassword,
 | 
				
			||||||
 | 
					    mdiServerOutline,
 | 
				
			||||||
    mdiTwoFactorAuthentication,
 | 
					    mdiTwoFactorAuthentication,
 | 
				
			||||||
  } from '@mdi/js';
 | 
					  } from '@mdi/js';
 | 
				
			||||||
 | 
					  import UserUsageStatistic from '$lib/components/user-settings-page/user-usage-statistic.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  interface Props {
 | 
					  interface Props {
 | 
				
			||||||
    keys?: ApiKeyResponseDto[];
 | 
					    keys?: ApiKeyResponseDto[];
 | 
				
			||||||
@ -59,6 +61,15 @@
 | 
				
			|||||||
    <UserProfileSettings />
 | 
					    <UserProfileSettings />
 | 
				
			||||||
  </SettingAccordion>
 | 
					  </SettingAccordion>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <SettingAccordion
 | 
				
			||||||
 | 
					    icon={mdiServerOutline}
 | 
				
			||||||
 | 
					    key="user-usage-info"
 | 
				
			||||||
 | 
					    title={$t('user_usage_stats')}
 | 
				
			||||||
 | 
					    subtitle={$t('user_usage_stats_description')}
 | 
				
			||||||
 | 
					  >
 | 
				
			||||||
 | 
					    <UserUsageStatistic />
 | 
				
			||||||
 | 
					  </SettingAccordion>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <SettingAccordion icon={mdiApi} key="api-keys" title={$t('api_keys')} subtitle={$t('manage_your_api_keys')}>
 | 
					  <SettingAccordion icon={mdiApi} key="api-keys" title={$t('api_keys')} subtitle={$t('manage_your_api_keys')}>
 | 
				
			||||||
    <UserAPIKeyList bind:keys />
 | 
					    <UserAPIKeyList bind:keys />
 | 
				
			||||||
  </SettingAccordion>
 | 
					  </SettingAccordion>
 | 
				
			||||||
 | 
				
			|||||||
@ -0,0 +1,124 @@
 | 
				
			|||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					  import {
 | 
				
			||||||
 | 
					    getAlbumStatistics,
 | 
				
			||||||
 | 
					    getAssetStatistics,
 | 
				
			||||||
 | 
					    type AlbumStatisticsResponseDto,
 | 
				
			||||||
 | 
					    type AssetStatsResponseDto,
 | 
				
			||||||
 | 
					  } from '@immich/sdk';
 | 
				
			||||||
 | 
					  import { onMount } from 'svelte';
 | 
				
			||||||
 | 
					  import { t } from 'svelte-i18n';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let timelineStats: AssetStatsResponseDto = $state({
 | 
				
			||||||
 | 
					    videos: 0,
 | 
				
			||||||
 | 
					    images: 0,
 | 
				
			||||||
 | 
					    total: 0,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let favoriteStats: AssetStatsResponseDto = $state({
 | 
				
			||||||
 | 
					    videos: 0,
 | 
				
			||||||
 | 
					    images: 0,
 | 
				
			||||||
 | 
					    total: 0,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let archiveStats: AssetStatsResponseDto = $state({
 | 
				
			||||||
 | 
					    videos: 0,
 | 
				
			||||||
 | 
					    images: 0,
 | 
				
			||||||
 | 
					    total: 0,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let trashStats: AssetStatsResponseDto = $state({
 | 
				
			||||||
 | 
					    videos: 0,
 | 
				
			||||||
 | 
					    images: 0,
 | 
				
			||||||
 | 
					    total: 0,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  let albumStats: AlbumStatisticsResponseDto = $state({
 | 
				
			||||||
 | 
					    owned: 0,
 | 
				
			||||||
 | 
					    shared: 0,
 | 
				
			||||||
 | 
					    notShared: 0,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const getUsage = async () => {
 | 
				
			||||||
 | 
					    [timelineStats, favoriteStats, archiveStats, trashStats, albumStats] = await Promise.all([
 | 
				
			||||||
 | 
					      getAssetStatistics({ isArchived: false }),
 | 
				
			||||||
 | 
					      getAssetStatistics({ isFavorite: true }),
 | 
				
			||||||
 | 
					      getAssetStatistics({ isArchived: true }),
 | 
				
			||||||
 | 
					      getAssetStatistics({ isTrashed: true }),
 | 
				
			||||||
 | 
					      getAlbumStatistics(),
 | 
				
			||||||
 | 
					    ]);
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  onMount(async () => {
 | 
				
			||||||
 | 
					    await getUsage();
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{#snippet row(viewName: string, imageCount: number, videoCount: number, totalCount: number)}
 | 
				
			||||||
 | 
					  <td class="w-1/4 text-ellipsis px-4 text-sm">{viewName}</td>
 | 
				
			||||||
 | 
					  <td class="w-1/4 text-ellipsis px-4 text-sm">{imageCount}</td>
 | 
				
			||||||
 | 
					  <td class="flex flex-row flex-wrap justify-center gap-x-2 gap-y-1 w-1/4"> {videoCount}</td>
 | 
				
			||||||
 | 
					  <td class="flex flex-row flex-wrap justify-center gap-x-2 gap-y-1 w-1/4"> {totalCount}</td>
 | 
				
			||||||
 | 
					{/snippet}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<section class="my-6">
 | 
				
			||||||
 | 
					  <p class="text-xs dark:text-white uppercase">{$t('photos_and_videos')}</p>
 | 
				
			||||||
 | 
					  <table class="w-full text-left mt-4">
 | 
				
			||||||
 | 
					    <thead
 | 
				
			||||||
 | 
					      class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <tr class="flex w-full place-items-center">
 | 
				
			||||||
 | 
					        <th class="w-1/4 text-center text-sm font-medium">{$t('view').toLocaleString()}</th>
 | 
				
			||||||
 | 
					        <th class="w-1/4 text-center text-sm font-medium">{$t('photos').toLocaleString()}</th>
 | 
				
			||||||
 | 
					        <th class="w-1/4 text-center text-sm font-medium">{$t('videos').toLocaleString()}</th>
 | 
				
			||||||
 | 
					        <th class="w-1/4 text-center text-sm font-medium">{$t('total').toLocaleString()}</th>
 | 
				
			||||||
 | 
					      </tr>
 | 
				
			||||||
 | 
					    </thead>
 | 
				
			||||||
 | 
					    <tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
 | 
				
			||||||
 | 
					      <tr
 | 
				
			||||||
 | 
					        class="flex h-[60px] w-full place-items-center text-center dark:text-immich-dark-fg bg-immich-bg dark:bg-immich-dark-gray/50"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {@render row($t('timeline'), timelineStats.images, timelineStats.videos, timelineStats.total)}
 | 
				
			||||||
 | 
					      </tr>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <tr
 | 
				
			||||||
 | 
					        class="flex h-[60px] w-full place-items-center text-center dark:text-immich-dark-fg bg-immich-gray dark:bg-immich-dark-gray/75"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {@render row($t('favorites'), favoriteStats.images, favoriteStats.videos, favoriteStats.total)}
 | 
				
			||||||
 | 
					      </tr>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <tr
 | 
				
			||||||
 | 
					        class="flex h-[60px] w-full place-items-center text-center dark:text-immich-dark-fg bg-immich-bg dark:bg-immich-dark-gray/50"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {@render row($t('archive'), archiveStats.images, archiveStats.videos, archiveStats.total)}
 | 
				
			||||||
 | 
					      </tr>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      <tr
 | 
				
			||||||
 | 
					        class="flex h-[60px] w-full place-items-center text-center dark:text-immich-dark-fg bg-immich-gray dark:bg-immich-dark-gray/75"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        {@render row($t('trash'), trashStats.images, trashStats.videos, trashStats.total)}
 | 
				
			||||||
 | 
					      </tr>
 | 
				
			||||||
 | 
					    </tbody>
 | 
				
			||||||
 | 
					  </table>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  <div class="mt-6">
 | 
				
			||||||
 | 
					    <p class="text-xs dark:text-white uppercase">{$t('albums')}</p>
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					  <table class="w-full text-left mt-4">
 | 
				
			||||||
 | 
					    <thead
 | 
				
			||||||
 | 
					      class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
 | 
				
			||||||
 | 
					    >
 | 
				
			||||||
 | 
					      <tr class="flex w-full place-items-center">
 | 
				
			||||||
 | 
					        <th class="w-1/2 text-center text-sm font-medium">{$t('owned')}</th>
 | 
				
			||||||
 | 
					        <th class="w-1/2 text-center text-sm font-medium">{$t('shared')}</th>
 | 
				
			||||||
 | 
					      </tr>
 | 
				
			||||||
 | 
					    </thead>
 | 
				
			||||||
 | 
					    <tbody class="block w-full overflow-y-auto rounded-md border dark:border-immich-dark-gray">
 | 
				
			||||||
 | 
					      <tr
 | 
				
			||||||
 | 
					        class="flex h-[60px] w-full place-items-center text-center dark:text-immich-dark-fg bg-immich-bg dark:bg-immich-dark-gray/50"
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <td class="w-1/2 text-ellipsis px-4 text-sm"> {albumStats.owned.toLocaleString()}</td>
 | 
				
			||||||
 | 
					        <td class="w-1/2 text-ellipsis px-4 text-sm">{albumStats.shared.toLocaleString()}</td>
 | 
				
			||||||
 | 
					      </tr>
 | 
				
			||||||
 | 
					    </tbody>
 | 
				
			||||||
 | 
					  </table>
 | 
				
			||||||
 | 
					</section>
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user