mirror of
https://github.com/immich-app/immich
synced 2025-11-14 17:36:12 +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
5
web/src/app.d.ts
vendored
5
web/src/app.d.ts
vendored
|
|
@ -27,3 +27,8 @@ declare namespace svelteHTML {
|
|||
'on:zoomImage'?: () => void;
|
||||
}
|
||||
}
|
||||
|
||||
declare module '$env/static/public' {
|
||||
export const PUBLIC_IMMICH_PAY_HOST: string;
|
||||
export const PUBLIC_IMMICH_BUY_HOST: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
} else if (width === 'narrow') {
|
||||
modalWidth = 'w-[28rem]';
|
||||
} else {
|
||||
modalWidth = 'sm:max-w-lg';
|
||||
modalWidth = 'sm:max-w-4xl';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { mdiPartyPopper } from '@mdi/js';
|
||||
|
||||
export let onDone: () => void;
|
||||
</script>
|
||||
|
||||
<div class="m-auto w-3/4 text-center flex flex-col place-content-center place-items-center mb-6 dark:text-white">
|
||||
<Icon path={mdiPartyPopper} class="text-immich-primary dark:text-immich-dark-primary" size="96" />
|
||||
<p class="text-4xl mt-8 font-bold">{$t('license_activated_title')}</p>
|
||||
<p class="text-lg mt-6">{$t('license_activated_subtitle')}</p>
|
||||
|
||||
<div class="mt-10 w-full">
|
||||
<Button fullwidth on:click={onDone}>OK</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<script lang="ts">
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import ServerLicenseCard from './server-license-card.svelte';
|
||||
import UserLicenseCard from './user-license-card.svelte';
|
||||
import { activateLicense, getActivationKey } from '$lib/utils/license-utils';
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
import { licenseStore } from '$lib/stores/license.store';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let onActivate: () => void;
|
||||
|
||||
let licenseKey = '';
|
||||
let isLoading = false;
|
||||
|
||||
const activate = async () => {
|
||||
try {
|
||||
licenseKey = licenseKey.trim();
|
||||
isLoading = true;
|
||||
|
||||
const activationKey = await getActivationKey(licenseKey);
|
||||
await activateLicense(licenseKey, activationKey);
|
||||
|
||||
onActivate();
|
||||
licenseStore.setLicenseStatus(true);
|
||||
} catch (error) {
|
||||
handleError(error, $t('license_failed_activation'));
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="p-4">
|
||||
<div>
|
||||
<h1 class="text-4xl font-bold text-immich-primary dark:text-immich-dark-primary tracking-wider">
|
||||
{$t('license_license_title')}
|
||||
</h1>
|
||||
<p class="text-lg mt-2 dark:text-immich-gray">{$t('license_license_subtitle')}</p>
|
||||
</div>
|
||||
<div class="flex gap-6 mt-4 justify-between">
|
||||
{#if $user.isAdmin}
|
||||
<ServerLicenseCard />
|
||||
{/if}
|
||||
<UserLicenseCard />
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<p class="dark:text-immich-gray">{$t('license_input_suggestion')}</p>
|
||||
<form class="mt-2 flex gap-2" on:submit={activate}>
|
||||
<input
|
||||
class="immich-form-input w-full"
|
||||
id="licensekey"
|
||||
type="text"
|
||||
bind:value={licenseKey}
|
||||
required
|
||||
placeholder="IMCL-0KEY-0CAN-00BE-FOUD-FROM-YOUR-EMAIL-INBX"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Button type="submit" rounded="lg"
|
||||
>{#if isLoading}
|
||||
<LoadingSpinner />
|
||||
{:else}
|
||||
{$t('license_button_activate')}
|
||||
{/if}</Button
|
||||
>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<script lang="ts">
|
||||
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||
import LicenseActivationSuccess from '$lib/components/shared-components/license/license-activation-success.svelte';
|
||||
import LicenseContent from '$lib/components/shared-components/license/license-content.svelte';
|
||||
|
||||
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
||||
|
||||
export let onClose: () => void;
|
||||
|
||||
let showLicenseActivated = false;
|
||||
</script>
|
||||
|
||||
<Portal>
|
||||
<FullScreenModal showLogo title={''} {onClose} width="wide">
|
||||
{#if showLicenseActivated}
|
||||
<LicenseActivationSuccess onDone={onClose} />
|
||||
{:else}
|
||||
<LicenseContent
|
||||
onActivate={() => {
|
||||
showLicenseActivated = true;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</FullScreenModal>
|
||||
</Portal>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<script lang="ts">
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { ImmichLicense } from '$lib/constants';
|
||||
import { getLicenseLink } from '$lib/utils/license-utils';
|
||||
import { mdiCheckCircleOutline, mdiServer } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<!-- SERVER LICENSE -->
|
||||
<div class="border border-gray-300 dark:border-gray-800 w-[375px] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900">
|
||||
<div class="text-immich-primary dark:text-immich-dark-primary">
|
||||
<Icon path={mdiServer} size="56" />
|
||||
<p class="font-semibold text-lg mt-1">{$t('license_server_title')}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 dark:text-immich-gray">
|
||||
<p class="text-6xl font-bold">$99<span class="text-2xl font-medium">.99</span></p>
|
||||
<p>{$t('license_per_server')}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-between h-[200px] dark:text-immich-gray">
|
||||
<div class="mt-6 flex flex-col gap-1">
|
||||
<div class="grid grid-cols-[36px_auto]">
|
||||
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
|
||||
<p class="self-center">{$t('license_server_description_1')}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-[36px_auto]">
|
||||
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
|
||||
<p class="self-center">{$t('license_lifetime_description')}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-[36px_auto]">
|
||||
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
|
||||
<p class="self-center">{$t('license_server_description_2')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href={getLicenseLink(ImmichLicense.Server)}>
|
||||
<Button fullwidth>{$t('license_button_select')}</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
<script lang="ts">
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { ImmichLicense } from '$lib/constants';
|
||||
import { getLicenseLink } from '$lib/utils/license-utils';
|
||||
import { mdiAccount, mdiCheckCircleOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<!-- USER LICENSE -->
|
||||
<div class="border border-gray-300 dark:border-gray-800 w-[375px] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900">
|
||||
<div class="text-immich-primary dark:text-immich-dark-primary">
|
||||
<Icon path={mdiAccount} size="56" />
|
||||
<p class="font-semibold text-lg mt-1">{$t('license_individual_title')}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 dark:text-immich-gray">
|
||||
<p class="text-6xl font-bold">$24<span class="text-2xl font-medium">.99</span></p>
|
||||
<p>{$t('license_per_user')}</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col justify-between h-[200px] dark:text-immich-gray">
|
||||
<div class="mt-6 flex flex-col gap-1">
|
||||
<div class="grid grid-cols-[36px_auto]">
|
||||
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
|
||||
<p class="self-center">{$t('license_individual_description_1')}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-[36px_auto]">
|
||||
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
|
||||
<p class="self-center">{$t('license_lifetime_description')}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href={getLicenseLink(ImmichLicense.Client)}>
|
||||
<Button fullwidth>{$t('license_button_select')}</Button>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -31,6 +31,7 @@
|
|||
|
||||
const logOut = async () => {
|
||||
const { redirectUri } = await logout();
|
||||
|
||||
if (redirectUri.startsWith('/')) {
|
||||
await goto(redirectUri);
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -45,6 +45,24 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<!--
|
||||
@component
|
||||
Allow rendering a component in a different part of the DOM.
|
||||
|
||||
### Props
|
||||
- `target` - HTMLElement i.e "body", "html", default is "body"
|
||||
|
||||
### Default Slot
|
||||
Used for every occurrence of an HTML tag in a message
|
||||
- `tag` - Name of the tag
|
||||
|
||||
@example
|
||||
```html
|
||||
<Portal target="body">
|
||||
<p>Your component in here</p>
|
||||
</Portal>
|
||||
```
|
||||
-->
|
||||
<script lang="ts">
|
||||
/**
|
||||
* DOM Element or CSS Selector
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
<script lang="ts">
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
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 { websocketStore } from '$lib/stores/websocket';
|
||||
import { requestServerInfo } from '$lib/utils/auth';
|
||||
import { mdiChartPie, mdiDns } from '@mdi/js';
|
||||
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';
|
||||
|
||||
const { serverVersion, connected } = websocketStore;
|
||||
|
||||
let usageClasses = '';
|
||||
let isOpen = false;
|
||||
|
||||
$: version = $serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null;
|
||||
$: 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="dark:text-immich-dark-fg">
|
||||
<div
|
||||
class="storage-status grid grid-cols-[64px_auto]"
|
||||
title={$t('storage_usage', {
|
||||
values: {
|
||||
used: getByteUnitString(usedBytes, $locale, 3),
|
||||
available: getByteUnitString(availableBytes, $locale, 3),
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div class="pb-[2.15rem] pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary group-hover:sm:pb-0 md:pb-0">
|
||||
<Icon path={mdiChartPie} size="24" />
|
||||
</div>
|
||||
<div class="hidden group-hover:sm:block md:block">
|
||||
<p class="text-sm font-medium text-immich-primary dark:text-immich-dark-primary">{$t('storage')}</p>
|
||||
{#if $serverInfo}
|
||||
<div class="my-2 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
|
||||
<div class="h-[7px] rounded-full {usageClasses}" style="width: {usedPercentage}%" />
|
||||
</div>
|
||||
<p class="text-xs">
|
||||
{$t('storage_usage', {
|
||||
values: {
|
||||
used: getByteUnitString(usedBytes, $locale),
|
||||
available: getByteUnitString(availableBytes, $locale),
|
||||
},
|
||||
})}
|
||||
</p>
|
||||
{:else}
|
||||
<div class="mt-2">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<hr class="my-4 ml-5 dark:border-immich-dark-gray" />
|
||||
</div>
|
||||
<div class="server-status grid grid-cols-[64px_auto]">
|
||||
<div class="pb-11 pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary group-hover:sm:pb-0 md:pb-0">
|
||||
<Icon path={mdiDns} size="26" />
|
||||
</div>
|
||||
<div class="hidden text-xs group-hover:sm:block md:block">
|
||||
<p class="text-sm font-medium text-immich-primary dark:text-immich-dark-primary">{$t('server')}</p>
|
||||
|
||||
<div class="mt-2 flex justify-between justify-items-center">
|
||||
<p>{$t('status')}</p>
|
||||
|
||||
{#if $connected}
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('online')}</p>
|
||||
{:else}
|
||||
<p class="font-medium text-red-500">{$t('offline')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="mt-2 flex justify-between justify-items-center">
|
||||
<p>{$t('version')}</p>
|
||||
{#if $connected && version}
|
||||
<button
|
||||
type="button"
|
||||
on:click={() => (isOpen = true)}
|
||||
class="font-medium text-immich-primary dark:text-immich-dark-primary">{version}</button
|
||||
>
|
||||
{:else}
|
||||
<p class="font-medium text-red-500">{$t('unknown')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,158 @@
|
|||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import { onMount } from 'svelte';
|
||||
import { licenseStore } from '$lib/stores/license.store';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import {
|
||||
deleteServerLicense,
|
||||
deleteUserLicense,
|
||||
getAboutInfo,
|
||||
getMyUser,
|
||||
getServerLicense,
|
||||
type LicenseResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { mdiLicense } from '@mdi/js';
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import LicenseContent from '$lib/components/shared-components/license/license-content.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { getAccountAge } from '$lib/utils/auth';
|
||||
const { isLicenseActivated } = licenseStore;
|
||||
|
||||
let isServerLicense = false;
|
||||
let serverLicenseInfo: LicenseResponseDto | null = null;
|
||||
const accountAge = getAccountAge();
|
||||
|
||||
const checkLicenseInfo = async () => {
|
||||
const serverInfo = await getAboutInfo();
|
||||
isServerLicense = serverInfo.licensed;
|
||||
|
||||
const userInfo = await getMyUser();
|
||||
if (userInfo.license) {
|
||||
$user = { ...$user, license: userInfo.license };
|
||||
}
|
||||
|
||||
if (isServerLicense && $user.isAdmin) {
|
||||
serverLicenseInfo = (await getServerLicense()) as LicenseResponseDto | null;
|
||||
}
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
if (!$isLicenseActivated) {
|
||||
return;
|
||||
}
|
||||
|
||||
await checkLicenseInfo();
|
||||
});
|
||||
|
||||
const removeUserLicense = async () => {
|
||||
try {
|
||||
const isConfirmed = await dialogController.show({
|
||||
title: 'Remove License',
|
||||
prompt: 'Are you sure you want to remove the license?',
|
||||
confirmText: 'Remove',
|
||||
cancelText: 'Cancel',
|
||||
});
|
||||
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteUserLicense();
|
||||
licenseStore.setLicenseStatus(false);
|
||||
} catch (error) {
|
||||
handleError(error, 'Failed to remove license');
|
||||
}
|
||||
};
|
||||
|
||||
const removeServerLicense = async () => {
|
||||
try {
|
||||
const isConfirmed = await dialogController.show({
|
||||
title: 'Remove License',
|
||||
prompt: 'Are you sure you want to remove the Server license?',
|
||||
confirmText: 'Remove',
|
||||
cancelText: 'Cancel',
|
||||
});
|
||||
|
||||
if (!isConfirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
await deleteServerLicense();
|
||||
licenseStore.setLicenseStatus(false);
|
||||
} catch (error) {
|
||||
handleError(error, 'Failed to remove license');
|
||||
}
|
||||
};
|
||||
|
||||
const onLicenseActivated = async () => {
|
||||
licenseStore.setLicenseStatus(true);
|
||||
await checkLicenseInfo();
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="my-4">
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
{#if $isLicenseActivated}
|
||||
{#if isServerLicense}
|
||||
<div
|
||||
class="bg-gray-50 border border-immich-dark-primary/50 dark:bg-immich-dark-primary/15 p-6 pr-12 rounded-xl flex place-content-center gap-4"
|
||||
>
|
||||
<Icon path={mdiLicense} size="56" class="text-immich-primary dark:text-immich-dark-primary" />
|
||||
|
||||
<div>
|
||||
<p class="text-immich-primary dark:text-immich-dark-primary font-semibold text-lg">Server License</p>
|
||||
|
||||
{#if $user.isAdmin && serverLicenseInfo?.activatedAt}
|
||||
<p class="dark:text-white text-sm mt-1 col-start-2">
|
||||
Activated on {new Date(serverLicenseInfo?.activatedAt).toLocaleDateString()}
|
||||
</p>
|
||||
{:else}
|
||||
<p class="dark:text-white">Your license is managed by the admin</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right mt-4">
|
||||
<Button size="sm" color="red" on:click={removeServerLicense}>Remove license</Button>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="bg-gray-50 border border-immich-dark-primary/50 dark:bg-immich-dark-primary/15 p-6 pr-12 rounded-xl flex place-content-center gap-4"
|
||||
>
|
||||
<Icon path={mdiLicense} size="56" class="text-immich-primary dark:text-immich-dark-primary" />
|
||||
|
||||
<div>
|
||||
<p class="text-immich-primary dark:text-immich-dark-primary font-semibold text-lg">Individual License</p>
|
||||
{#if $user.license?.activatedAt}
|
||||
<p class="dark:text-white text-sm mt-1 col-start-2">
|
||||
Activated on {new Date($user.license?.activatedAt).toLocaleDateString()}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right mt-4">
|
||||
<Button size="sm" color="red" on:click={removeUserLicense}>Remove license</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
{#if accountAge > 14}
|
||||
<div
|
||||
class="text-center bg-gray-100 border border-immich-dark-primary/50 dark:bg-immich-dark-primary/15 p-4 rounded-xl"
|
||||
>
|
||||
<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 } })}</span
|
||||
>. {$t('license_trial_info_4')}
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
<LicenseContent onActivate={onLicenseActivated} />
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
import NotificationsSettings from '$lib/components/user-settings-page/notifications-settings.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import DownloadSettings from '$lib/components/user-settings-page/download-settings.svelte';
|
||||
import LicenseSettings from '$lib/components/user-settings-page/license-settings.svelte';
|
||||
|
||||
export let keys: ApiKeyResponseDto[] = [];
|
||||
export let sessions: SessionResponseDto[] = [];
|
||||
|
|
@ -52,6 +53,14 @@
|
|||
<DownloadSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
key="user-license-settings"
|
||||
title={$t('user_license_settings')}
|
||||
subtitle={$t('user_license_settings_description')}
|
||||
>
|
||||
<LicenseSettings />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion key="memories" title={$t('memories')} subtitle={$t('memories_setting_description')}>
|
||||
<MemoriesSettings />
|
||||
</SettingAccordion>
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export enum AppRoute {
|
|||
MEMORY = '/memory',
|
||||
TRASH = '/trash',
|
||||
PARTNERS = '/partners',
|
||||
BUY = '/buy',
|
||||
|
||||
AUTH_LOGIN = '/auth/login',
|
||||
AUTH_REGISTER = '/auth/register',
|
||||
|
|
@ -309,3 +310,8 @@ export const langs = [
|
|||
},
|
||||
{ name: 'Development (keys only)', code: 'dev', loader: () => Promise.resolve({}) },
|
||||
];
|
||||
|
||||
export enum ImmichLicense {
|
||||
Client = 'immich-client',
|
||||
Server = 'immich-server',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -403,6 +403,7 @@
|
|||
"bulk_delete_duplicates_confirmation": "Are you sure you want to bulk delete {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will keep the largest asset of each group and permanently delete all other duplicates. You cannot undo this action!",
|
||||
"bulk_keep_duplicates_confirmation": "Are you sure you want to keep {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will resolve all duplicate groups without deleting anything.",
|
||||
"bulk_trash_duplicates_confirmation": "Are you sure you want to bulk trash {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will keep the largest asset of each group and trash all other duplicates.",
|
||||
"buy": "Purchase License",
|
||||
"camera": "Camera",
|
||||
"camera_brand": "Camera brand",
|
||||
"camera_model": "Camera model",
|
||||
|
|
@ -742,6 +743,31 @@
|
|||
"level": "Level",
|
||||
"library": "Library",
|
||||
"library_options": "Library options",
|
||||
"license_account_info": "Your account is licensed",
|
||||
"license_activated_subtitle": "Thank you for supporting Immich and open-source software",
|
||||
"license_activated_title": "Your license has been successfully activated",
|
||||
"license_button_activate": "Activate",
|
||||
"license_button_buy": "Buy",
|
||||
"license_button_buy_license": "Buy License",
|
||||
"license_button_select": "Select",
|
||||
"license_failed_activation": "Failed to activate license. Please check your email for the the correct license key!",
|
||||
"license_individual_description_1": "1 license per user on any server",
|
||||
"license_individual_title": "Individual License",
|
||||
"license_info_licensed": "Licensed",
|
||||
"license_info_unlicensed": "Unlicensed",
|
||||
"license_input_suggestion": "Have a license? Enter the key below",
|
||||
"license_license_subtitle": "Buy a license to support Immich",
|
||||
"license_license_title": "LICENSE",
|
||||
"license_lifetime_description": "Lifetime license",
|
||||
"license_per_server": "Per server",
|
||||
"license_per_user": "Per user",
|
||||
"license_server_description_1": "1 license per server",
|
||||
"license_server_description_2": "License for all users on the server",
|
||||
"license_server_title": "Server License",
|
||||
"license_trial_info_1": "You are running an Unlicensed version of Immich",
|
||||
"license_trial_info_2": "You have been using Immich for approximately",
|
||||
"license_trial_info_3": "{accountAge, plural, one {# day} other {# days}}",
|
||||
"license_trial_info_4": "Please considering purchasing a license to support the continued development of the service",
|
||||
"light": "Light",
|
||||
"like_deleted": "Like deleted",
|
||||
"link_options": "Link options",
|
||||
|
|
@ -1007,7 +1033,8 @@
|
|||
"selected_count": "{count, plural, other {# selected}}",
|
||||
"send_message": "Send message",
|
||||
"send_welcome_email": "Send welcome email",
|
||||
"server": "Server",
|
||||
"server_offline": "Server Offline",
|
||||
"server_online": "Server Online",
|
||||
"server_stats": "Server Stats",
|
||||
"server_version": "Server Version",
|
||||
"set": "Set",
|
||||
|
|
@ -1073,7 +1100,7 @@
|
|||
"stop_photo_sharing": "Stop sharing your photos?",
|
||||
"stop_photo_sharing_description": "{partner} will no longer be able to access your photos.",
|
||||
"stop_sharing_photos_with_user": "Stop sharing your photos with this user",
|
||||
"storage": "Storage",
|
||||
"storage": "Storage space",
|
||||
"storage_label": "Storage label",
|
||||
"storage_usage": "{used} of {available} used",
|
||||
"submit": "Submit",
|
||||
|
|
@ -1136,6 +1163,8 @@
|
|||
"use_custom_date_range": "Use custom date range instead",
|
||||
"user": "User",
|
||||
"user_id": "User ID",
|
||||
"user_license_settings": "License",
|
||||
"user_license_settings_description": "Manage your license",
|
||||
"user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}",
|
||||
"user_role_set": "Set {user} as {role}",
|
||||
"user_usage_detail": "User usage detail",
|
||||
|
|
|
|||
18
web/src/lib/stores/license.store.ts
Normal file
18
web/src/lib/stores/license.store.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { writable } from 'svelte/store';
|
||||
|
||||
function createLicenseStore() {
|
||||
const isLicenseActivated = writable(false);
|
||||
|
||||
function setLicenseStatus(status: boolean) {
|
||||
isLicenseActivated.set(status);
|
||||
}
|
||||
|
||||
return {
|
||||
isLicenseActivated: {
|
||||
subscribe: isLicenseActivated.subscribe,
|
||||
},
|
||||
setLicenseStatus,
|
||||
};
|
||||
}
|
||||
|
||||
export const licenseStore = createLicenseStore();
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { licenseStore } from '$lib/stores/license.store';
|
||||
import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
|
|
@ -11,4 +12,5 @@ export const preferences = writable<UserPreferencesResponseDto>();
|
|||
export const resetSavedUser = () => {
|
||||
user.set(undefined as unknown as UserAdminResponseDto);
|
||||
preferences.set(undefined as unknown as UserPreferencesResponseDto);
|
||||
licenseStore.setLicenseStatus(false);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { browser } from '$app/environment';
|
||||
import { licenseStore } from '$lib/stores/license.store';
|
||||
import { serverInfo } from '$lib/stores/server-info.store';
|
||||
import { preferences as preferences$, user as user$ } from '$lib/stores/user.store';
|
||||
import { getMyPreferences, getMyUser, getStorage } from '@immich/sdk';
|
||||
import { getAboutInfo, getMyPreferences, getMyUser, getStorage } from '@immich/sdk';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { DateTime } from 'luxon';
|
||||
import { get } from 'svelte/store';
|
||||
import { AppRoute } from '../constants';
|
||||
|
||||
|
|
@ -15,10 +17,17 @@ export const loadUser = async () => {
|
|||
try {
|
||||
let user = get(user$);
|
||||
let preferences = get(preferences$);
|
||||
let serverInfo;
|
||||
|
||||
if ((!user || !preferences) && hasAuthCookie()) {
|
||||
[user, preferences] = await Promise.all([getMyUser(), getMyPreferences()]);
|
||||
[user, preferences, serverInfo] = await Promise.all([getMyUser(), getMyPreferences(), getAboutInfo()]);
|
||||
user$.set(user);
|
||||
preferences$.set(preferences);
|
||||
|
||||
// Check for license status
|
||||
if (serverInfo.licensed || user.license?.activatedAt) {
|
||||
licenseStore.setLicenseStatus(true);
|
||||
}
|
||||
}
|
||||
return user;
|
||||
} catch {
|
||||
|
|
@ -64,3 +73,17 @@ export const requestServerInfo = async () => {
|
|||
serverInfo.set(data);
|
||||
}
|
||||
};
|
||||
|
||||
export const getAccountAge = (): number => {
|
||||
const user = get(user$);
|
||||
|
||||
if (!user) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const createdDate = DateTime.fromISO(user.createdAt);
|
||||
const now = DateTime.now();
|
||||
const accountAge = now.diff(createdDate, 'days').days.toFixed(0);
|
||||
|
||||
return Number(accountAge);
|
||||
};
|
||||
|
|
|
|||
26
web/src/lib/utils/license-utils.ts
Normal file
26
web/src/lib/utils/license-utils.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { PUBLIC_IMMICH_BUY_HOST, PUBLIC_IMMICH_PAY_HOST } from '$env/static/public';
|
||||
import type { ImmichLicense } from '$lib/constants';
|
||||
import { serverConfig } from '$lib/stores/server-config.store';
|
||||
import { setServerLicense, setUserLicense, type LicenseResponseDto } from '@immich/sdk';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
export const activateLicense = async (licenseKey: string, activationKey: string): Promise<LicenseResponseDto> => {
|
||||
const isServerKey = licenseKey.search('IMSV') !== -1;
|
||||
const licenseKeyDto = { licenseKey, activationKey };
|
||||
return isServerKey ? setServerLicense({ licenseKeyDto }) : setUserLicense({ licenseKeyDto });
|
||||
};
|
||||
|
||||
export const getActivationKey = async (licenseKey: string): Promise<string> => {
|
||||
const response = await fetch(new URL(`/api/v1/activate/${licenseKey}`, PUBLIC_IMMICH_PAY_HOST).href);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch activation key');
|
||||
}
|
||||
return response.text();
|
||||
};
|
||||
|
||||
export const getLicenseLink = (license: ImmichLicense) => {
|
||||
const url = new URL('/', PUBLIC_IMMICH_BUY_HOST);
|
||||
url.searchParams.append('productId', license);
|
||||
url.searchParams.append('instanceUrl', get(serverConfig).externalDomain || window.origin);
|
||||
return url.href;
|
||||
};
|
||||
53
web/src/routes/(user)/buy/+page.svelte
Normal file
53
web/src/routes/(user)/buy/+page.svelte
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||
import LicenseActivationSuccess from '$lib/components/shared-components/license/license-activation-success.svelte';
|
||||
import LicenseContent from '$lib/components/shared-components/license/license-content.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { mdiAlertCircleOutline, mdiLicense } from '@mdi/js';
|
||||
import { licenseStore } from '$lib/stores/license.store';
|
||||
|
||||
export let data: PageData;
|
||||
let showLicenseActivated = false;
|
||||
const { isLicenseActivated } = licenseStore;
|
||||
</script>
|
||||
|
||||
<UserPageLayout title={$t('buy')}>
|
||||
<section class="mx-4 flex place-content-center">
|
||||
<div class={`w-full ${$user.isAdmin ? 'max-w-3xl' : 'max-w-xl'}`}>
|
||||
{#if data.isActivated === false}
|
||||
<div
|
||||
class="bg-red-100 text-red-700 px-4 py-3 rounded-md flex place-items-center place-content-center gap-2"
|
||||
role="alert"
|
||||
>
|
||||
<Icon path={mdiAlertCircleOutline} size="18" />
|
||||
<p>{$t('license_failed_activation')}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if $isLicenseActivated}
|
||||
<div
|
||||
class="bg-immich-primary/10 text-immich-primary px-4 py-3 rounded-md flex place-items-center place-content-center gap-2 mb-5 dark:text-black dark:bg-immich-dark-primary"
|
||||
role="alert"
|
||||
>
|
||||
<Icon path={mdiLicense} size="24" />
|
||||
<p>{$t('license_account_info')}</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if showLicenseActivated || data.isActivated === true}
|
||||
<LicenseActivationSuccess onDone={() => goto(AppRoute.PHOTOS, { replaceState: false })} />
|
||||
{:else}
|
||||
<LicenseContent
|
||||
onActivate={() => {
|
||||
showLicenseActivated = true;
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
</UserPageLayout>
|
||||
38
web/src/routes/(user)/buy/+page.ts
Normal file
38
web/src/routes/(user)/buy/+page.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { licenseStore } from '$lib/stores/license.store';
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { activateLicense, getActivationKey } from '$lib/utils/license-utils';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ url }) => {
|
||||
await authenticate();
|
||||
|
||||
const $t = await getFormatter();
|
||||
const licenseKey = url.searchParams.get('licenseKey');
|
||||
let activationKey = url.searchParams.get('activationKey');
|
||||
let isActivated: boolean | undefined = undefined;
|
||||
|
||||
try {
|
||||
if (licenseKey && !activationKey) {
|
||||
activationKey = await getActivationKey(licenseKey);
|
||||
}
|
||||
|
||||
if (licenseKey && activationKey) {
|
||||
const response = await activateLicense(licenseKey, activationKey);
|
||||
if (response.activatedAt !== '') {
|
||||
isActivated = true;
|
||||
licenseStore.setLicenseStatus(true);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
isActivated = false;
|
||||
console.log('error navigating to /buy', error);
|
||||
}
|
||||
|
||||
return {
|
||||
meta: {
|
||||
title: $t('buy'),
|
||||
},
|
||||
isActivated,
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
|
|
@ -22,7 +22,6 @@
|
|||
import { t } from 'svelte-i18n';
|
||||
|
||||
let showNavigationLoadingBar = false;
|
||||
|
||||
$: changeTheme($colorTheme);
|
||||
|
||||
$: if ($user) {
|
||||
|
|
|
|||
|
|
@ -7,11 +7,11 @@ export const load = (({ url }) => {
|
|||
HOME = 'home',
|
||||
UNSUBSCRIBE = 'unsubscribe',
|
||||
VIEW_ASSET = 'view_asset',
|
||||
ACTIVATE_LICENSE = 'activate_license',
|
||||
}
|
||||
|
||||
const queryParams = url.searchParams;
|
||||
const target = queryParams.get('target') as LinkTarget;
|
||||
|
||||
switch (target) {
|
||||
case LinkTarget.HOME: {
|
||||
return redirect(302, AppRoute.PHOTOS);
|
||||
|
|
@ -28,6 +28,26 @@ export const load = (({ url }) => {
|
|||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case LinkTarget.ACTIVATE_LICENSE: {
|
||||
// https://my.immich.app/link?target=activate_license&licenseKey=IMCL-76S5-B4KG-4HXA-KRQF-C1G1-7PJ6-9V9V-7WQH
|
||||
// https://my.immich.app/link?target=activate_license&licenseKey=IMCL-9XC3-T4S3-37BU-GGJ5-8MWP-F2Y1-BGEX-AQTF
|
||||
const licenseKey = queryParams.get('licenseKey');
|
||||
const activationKey = queryParams.get('activationKey');
|
||||
const redirectUrl = new URL(AppRoute.BUY, url.origin);
|
||||
|
||||
if (licenseKey) {
|
||||
redirectUrl.searchParams.append('licenseKey', licenseKey);
|
||||
|
||||
if (activationKey) {
|
||||
redirectUrl.searchParams.append('activationKey', activationKey);
|
||||
}
|
||||
|
||||
return redirect(302, redirectUrl);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return redirect(302, AppRoute.PHOTOS);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue