mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
feat(web): license UI (#11182)
This commit is contained in:
parent
88f62087fd
commit
ef0e1a81b9
39 changed files with 1157 additions and 148 deletions
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
|
||||
import SideBarLink from '$lib/components/shared-components/side-bar/side-bar-link.svelte';
|
||||
import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
|
||||
import StatusBox from '$lib/components/shared-components/status-box.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiTools } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
|
@ -17,7 +17,5 @@
|
|||
<SideBarLink title={$t('repair')} routeId={AppRoute.ADMIN_REPAIR} icon={mdiTools} preloadData={false} />
|
||||
</nav>
|
||||
|
||||
<div class="mb-6 mt-auto">
|
||||
<StatusBox />
|
||||
</div>
|
||||
<BottomInfo />
|
||||
</SideBarSection>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
import LicenseInfo from './license-info.svelte';
|
||||
import ServerStatus from './server-status.svelte';
|
||||
import StorageSpace from './storage-space.svelte';
|
||||
</script>
|
||||
|
||||
<div class="mt-auto">
|
||||
<StorageSpace />
|
||||
</div>
|
||||
|
||||
<div class="mb-2">
|
||||
<LicenseInfo />
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<ServerStatus />
|
||||
</div>
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
<script lang="ts">
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { mdiClose, mdiInformationOutline, mdiLicense } from '@mdi/js';
|
||||
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import LicenseModal from '$lib/components/shared-components/license/license-modal.svelte';
|
||||
import { licenseStore } from '$lib/stores/license.store';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { goto } from '$app/navigation';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { getAccountAge } from '$lib/utils/auth';
|
||||
|
||||
let showMessage = false;
|
||||
let isOpen = false;
|
||||
const { isLicenseActivated } = licenseStore;
|
||||
|
||||
const openLicenseModal = () => {
|
||||
isOpen = true;
|
||||
showMessage = false;
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<LicenseModal onClose={() => (isOpen = false)} />
|
||||
{/if}
|
||||
|
||||
<div class="hidden md:block license-status pl-4 text-sm">
|
||||
{#if $isLicenseActivated}
|
||||
<button
|
||||
on:click={() => goto(`${AppRoute.USER_SETTINGS}?isOpen=user-license-settings`)}
|
||||
class="w-full"
|
||||
type="button"
|
||||
>
|
||||
<div class="flex gap-1 mt-2 place-items-center dark:bg-immich-dark-primary/10 bg-gray-100 py-3 px-2 rounded-lg">
|
||||
<Icon path={mdiLicense} size="18" class="text-immich-primary dark:text-immich-dark-primary" />
|
||||
<p class="dark:text-gray-100">{$t('license_info_licensed')}</p>
|
||||
</div>
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
type="button"
|
||||
on:click={openLicenseModal}
|
||||
on:mouseenter={() => (showMessage = true)}
|
||||
class="py-3 px-2 flex justify-between place-items-center place-content-center border border-gray-300 dark:border-immich-dark-primary/50 mt-2 rounded-lg shadow-sm dark:bg-immich-dark-primary/10 w-full"
|
||||
>
|
||||
<div class="flex place-items-center place-content-center gap-1">
|
||||
<Icon path={mdiLicense} size="18" class="text-immich-dark-gray/75 dark:text-immich-gray/85" />
|
||||
<p class="text-immich-dark-gray/75 dark:text-immich-gray">{$t('license_info_unlicensed')}</p>
|
||||
</div>
|
||||
|
||||
<div class="text-immich-primary dark:text-immich-dark-primary flex place-items-center gap-[2px] font-medium">
|
||||
{$t('license_button_buy')}
|
||||
|
||||
<span role="contentinfo">
|
||||
<Icon path={mdiInformationOutline}></Icon>
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<Portal target="body">
|
||||
{#if showMessage && getAccountAge() > 14}
|
||||
<div
|
||||
class="w-[265px] absolute bottom-[75px] left-[255px] bg-white dark:bg-gray-800 dark:text-white text-black rounded-xl z-10 shadow-2xl px-4 py-5"
|
||||
>
|
||||
<div class="flex justify-between place-items-center">
|
||||
<Icon path={mdiLicense} size="44" class="text-immich-dark-gray/75 dark:text-immich-gray" />
|
||||
<CircleIconButton
|
||||
icon={mdiClose}
|
||||
on:click={() => {
|
||||
showMessage = false;
|
||||
}}
|
||||
title="Close"
|
||||
size="18"
|
||||
class="text-immich-dark-gray/85 dark:text-immich-gray"
|
||||
/>
|
||||
</div>
|
||||
<h1 class="text-lg font-medium my-3">{$t('license_trial_info_1')}</h1>
|
||||
<p class="text-immich-dark-gray/80 dark:text-immich-gray text-balance">
|
||||
{$t('license_trial_info_2')}
|
||||
<span class="text-immich-primary dark:text-immich-dark-primary font-semibold">
|
||||
{$t('license_trial_info_3', { values: { accountAge: getAccountAge() } })}</span
|
||||
>. {$t('license_trial_info_4')}
|
||||
</p>
|
||||
<div class="mt-3">
|
||||
<Button size="sm" fullwidth on:click={openLicenseModal}>{$t('license_button_buy_license')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Portal>
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
<script lang="ts">
|
||||
import ServerAboutModal from '$lib/components/shared-components/server-about-modal.svelte';
|
||||
import { websocketStore } from '$lib/stores/websocket';
|
||||
import { requestServerInfo } from '$lib/utils/auth';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
|
||||
|
||||
const { serverVersion, connected } = websocketStore;
|
||||
|
||||
let isOpen = false;
|
||||
|
||||
$: version = $serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null;
|
||||
|
||||
let aboutInfo: ServerAboutResponseDto;
|
||||
|
||||
onMount(async () => {
|
||||
await requestServerInfo();
|
||||
aboutInfo = await getAboutInfo();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<ServerAboutModal onClose={() => (isOpen = false)} info={aboutInfo} />
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="text-sm hidden group-hover:sm:flex md:flex pl-5 pr-1 place-items-center place-content-center justify-between"
|
||||
>
|
||||
{#if $connected}
|
||||
<div class="flex gap-2 place-items-center place-content-center">
|
||||
<div class="w-[7px] h-[7px] bg-green-500 rounded-full" />
|
||||
<p class="dark:text-immich-gray">{$t('server_online')}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex gap-2 place-items-center place-content-center">
|
||||
<div class="w-[7px] h-[7px] bg-red-500 rounded-full" />
|
||||
<p class="text-red-500">{$t('server_offline')}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex justify-between justify-items-center">
|
||||
{#if $connected && version}
|
||||
<button type="button" on:click={() => (isOpen = true)} class="dark:text-immich-gray">{version}</button>
|
||||
{:else}
|
||||
<p class="text-red-500">{$t('unknown')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -21,12 +21,12 @@
|
|||
mdiToolbox,
|
||||
mdiToolboxOutline,
|
||||
} from '@mdi/js';
|
||||
import StatusBox from '../status-box.svelte';
|
||||
import SideBarSection from './side-bar-section.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 BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
|
||||
|
||||
let isArchiveSelected: boolean;
|
||||
let isFavoritesSelected: boolean;
|
||||
|
|
@ -136,8 +136,5 @@
|
|||
{/if}
|
||||
</nav>
|
||||
|
||||
<!-- Status Box -->
|
||||
<div class="mb-6 mt-auto">
|
||||
<StatusBox />
|
||||
</div>
|
||||
<BottomInfo />
|
||||
</SideBarSection>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,82 @@
|
|||
<script lang="ts">
|
||||
import ServerAboutModal from '$lib/components/shared-components/server-about-modal.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { serverInfo } from '$lib/stores/server-info.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { requestServerInfo } from '$lib/utils/auth';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { getByteUnitString } from '../../../utils/byte-units';
|
||||
import LoadingSpinner from '../loading-spinner.svelte';
|
||||
import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
|
||||
|
||||
let usageClasses = '';
|
||||
let isOpen = false;
|
||||
|
||||
$: hasQuota = $user?.quotaSizeInBytes !== null;
|
||||
$: availableBytes = (hasQuota ? $user?.quotaSizeInBytes : $serverInfo?.diskSizeRaw) || 0;
|
||||
$: usedBytes = (hasQuota ? $user?.quotaUsageInBytes : $serverInfo?.diskUseRaw) || 0;
|
||||
$: usedPercentage = Math.round((usedBytes / availableBytes) * 100);
|
||||
|
||||
let aboutInfo: ServerAboutResponseDto;
|
||||
|
||||
const onUpdate = () => {
|
||||
usageClasses = getUsageClass();
|
||||
};
|
||||
|
||||
const getUsageClass = () => {
|
||||
if (usedPercentage >= 95) {
|
||||
return 'bg-red-500';
|
||||
}
|
||||
|
||||
if (usedPercentage > 80) {
|
||||
return 'bg-yellow-500';
|
||||
}
|
||||
|
||||
return 'bg-immich-primary dark:bg-immich-dark-primary';
|
||||
};
|
||||
|
||||
$: $user && onUpdate();
|
||||
|
||||
onMount(async () => {
|
||||
await requestServerInfo();
|
||||
aboutInfo = await getAboutInfo();
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if isOpen}
|
||||
<ServerAboutModal onClose={() => (isOpen = false)} info={aboutInfo} />
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="hidden md:block storage-status p-4 bg-gray-100 dark:bg-immich-dark-primary/10 ml-4 rounded-lg text-sm"
|
||||
title={$t('storage_usage', {
|
||||
values: {
|
||||
used: getByteUnitString(usedBytes, $locale, 3),
|
||||
available: getByteUnitString(availableBytes, $locale, 3),
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div class="hidden group-hover:sm:block md:block">
|
||||
<p class="font-medium text-immich-dark-gray dark:text-white mb-2">{$t('storage')}</p>
|
||||
|
||||
{#if $serverInfo}
|
||||
<p class="text-gray-500 dark:text-gray-300">
|
||||
{$t('storage_usage', {
|
||||
values: {
|
||||
used: getByteUnitString(usedBytes, $locale),
|
||||
available: getByteUnitString(availableBytes, $locale),
|
||||
},
|
||||
})}
|
||||
</p>
|
||||
|
||||
<div class="mt-4 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div class="h-[7px] rounded-full {usageClasses}" style="width: {usedPercentage}%" />
|
||||
</div>
|
||||
{:else}
|
||||
<div class="mt-2">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
Loading…
Add table
Add a link
Reference in a new issue