mirror of
https://github.com/immich-app/immich.git
synced 2024-11-27 09:21:05 +02:00
chore(web): change license wording and other things (#11309)
This commit is contained in:
parent
bc20710c6d
commit
ef7a6bb246
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/lib/api.dart
generated
BIN
mobile/openapi/lib/api.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/api_client.dart
generated
BIN
mobile/openapi/lib/api_client.dart
generated
Binary file not shown.
BIN
mobile/openapi/lib/model/purchase_response.dart
generated
Normal file
BIN
mobile/openapi/lib/model/purchase_response.dart
generated
Normal file
Binary file not shown.
BIN
mobile/openapi/lib/model/purchase_update.dart
generated
Normal file
BIN
mobile/openapi/lib/model/purchase_update.dart
generated
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -9775,6 +9775,32 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"PurchaseResponse": {
|
||||||
|
"properties": {
|
||||||
|
"hideBuyButtonUntil": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"showSupportBadge": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"hideBuyButtonUntil",
|
||||||
|
"showSupportBadge"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"PurchaseUpdate": {
|
||||||
|
"properties": {
|
||||||
|
"hideBuyButtonUntil": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"showSupportBadge": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"QueueStatusDto": {
|
"QueueStatusDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"isActive": {
|
"isActive": {
|
||||||
@ -11742,13 +11768,17 @@
|
|||||||
},
|
},
|
||||||
"memories": {
|
"memories": {
|
||||||
"$ref": "#/components/schemas/MemoryResponse"
|
"$ref": "#/components/schemas/MemoryResponse"
|
||||||
|
},
|
||||||
|
"purchase": {
|
||||||
|
"$ref": "#/components/schemas/PurchaseResponse"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"avatar",
|
"avatar",
|
||||||
"download",
|
"download",
|
||||||
"emailNotifications",
|
"emailNotifications",
|
||||||
"memories"
|
"memories",
|
||||||
|
"purchase"
|
||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
@ -11765,6 +11795,9 @@
|
|||||||
},
|
},
|
||||||
"memories": {
|
"memories": {
|
||||||
"$ref": "#/components/schemas/MemoryUpdate"
|
"$ref": "#/components/schemas/MemoryUpdate"
|
||||||
|
},
|
||||||
|
"purchase": {
|
||||||
|
"$ref": "#/components/schemas/PurchaseUpdate"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"type": "object"
|
"type": "object"
|
||||||
|
@ -95,11 +95,16 @@ export type EmailNotificationsResponse = {
|
|||||||
export type MemoryResponse = {
|
export type MemoryResponse = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
export type PurchaseResponse = {
|
||||||
|
hideBuyButtonUntil: string;
|
||||||
|
showSupportBadge: boolean;
|
||||||
|
};
|
||||||
export type UserPreferencesResponseDto = {
|
export type UserPreferencesResponseDto = {
|
||||||
avatar: AvatarResponse;
|
avatar: AvatarResponse;
|
||||||
download: DownloadResponse;
|
download: DownloadResponse;
|
||||||
emailNotifications: EmailNotificationsResponse;
|
emailNotifications: EmailNotificationsResponse;
|
||||||
memories: MemoryResponse;
|
memories: MemoryResponse;
|
||||||
|
purchase: PurchaseResponse;
|
||||||
};
|
};
|
||||||
export type AvatarUpdate = {
|
export type AvatarUpdate = {
|
||||||
color?: UserAvatarColor;
|
color?: UserAvatarColor;
|
||||||
@ -115,11 +120,16 @@ export type EmailNotificationsUpdate = {
|
|||||||
export type MemoryUpdate = {
|
export type MemoryUpdate = {
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
};
|
};
|
||||||
|
export type PurchaseUpdate = {
|
||||||
|
hideBuyButtonUntil?: string;
|
||||||
|
showSupportBadge?: boolean;
|
||||||
|
};
|
||||||
export type UserPreferencesUpdateDto = {
|
export type UserPreferencesUpdateDto = {
|
||||||
avatar?: AvatarUpdate;
|
avatar?: AvatarUpdate;
|
||||||
download?: DownloadUpdate;
|
download?: DownloadUpdate;
|
||||||
emailNotifications?: EmailNotificationsUpdate;
|
emailNotifications?: EmailNotificationsUpdate;
|
||||||
memories?: MemoryUpdate;
|
memories?: MemoryUpdate;
|
||||||
|
purchase?: PurchaseUpdate;
|
||||||
};
|
};
|
||||||
export type AlbumUserResponseDto = {
|
export type AlbumUserResponseDto = {
|
||||||
role: AlbumUserRole;
|
role: AlbumUserRole;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
import { Type } from 'class-transformer';
|
import { Type } from 'class-transformer';
|
||||||
import { IsEnum, IsInt, IsPositive, ValidateNested } from 'class-validator';
|
import { IsDateString, IsEnum, IsInt, IsPositive, ValidateNested } from 'class-validator';
|
||||||
import { UserAvatarColor, UserPreferences } from 'src/entities/user-metadata.entity';
|
import { UserAvatarColor, UserPreferences } from 'src/entities/user-metadata.entity';
|
||||||
import { Optional, ValidateBoolean } from 'src/validation';
|
import { Optional, ValidateBoolean } from 'src/validation';
|
||||||
|
|
||||||
@ -35,6 +35,15 @@ class DownloadUpdate {
|
|||||||
archiveSize?: number;
|
archiveSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PurchaseUpdate {
|
||||||
|
@ValidateBoolean({ optional: true })
|
||||||
|
showSupportBadge?: boolean;
|
||||||
|
|
||||||
|
@IsDateString()
|
||||||
|
@Optional()
|
||||||
|
hideBuyButtonUntil?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class UserPreferencesUpdateDto {
|
export class UserPreferencesUpdateDto {
|
||||||
@Optional()
|
@Optional()
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@ -55,6 +64,11 @@ export class UserPreferencesUpdateDto {
|
|||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@Type(() => DownloadUpdate)
|
@Type(() => DownloadUpdate)
|
||||||
download?: DownloadUpdate;
|
download?: DownloadUpdate;
|
||||||
|
|
||||||
|
@Optional()
|
||||||
|
@ValidateNested()
|
||||||
|
@Type(() => PurchaseUpdate)
|
||||||
|
purchase?: PurchaseUpdate;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AvatarResponse {
|
class AvatarResponse {
|
||||||
@ -77,11 +91,17 @@ class DownloadResponse {
|
|||||||
archiveSize!: number;
|
archiveSize!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class PurchaseResponse {
|
||||||
|
showSupportBadge!: boolean;
|
||||||
|
hideBuyButtonUntil!: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class UserPreferencesResponseDto implements UserPreferences {
|
export class UserPreferencesResponseDto implements UserPreferences {
|
||||||
memories!: MemoryResponse;
|
memories!: MemoryResponse;
|
||||||
avatar!: AvatarResponse;
|
avatar!: AvatarResponse;
|
||||||
emailNotifications!: EmailNotificationsResponse;
|
emailNotifications!: EmailNotificationsResponse;
|
||||||
download!: DownloadResponse;
|
download!: DownloadResponse;
|
||||||
|
purchase!: PurchaseResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => {
|
export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => {
|
||||||
|
@ -45,6 +45,10 @@ export interface UserPreferences {
|
|||||||
download: {
|
download: {
|
||||||
archiveSize: number;
|
archiveSize: number;
|
||||||
};
|
};
|
||||||
|
purchase: {
|
||||||
|
showSupportBadge: boolean;
|
||||||
|
hideBuyButtonUntil: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getDefaultPreferences = (user: { email: string }): UserPreferences => {
|
export const getDefaultPreferences = (user: { email: string }): UserPreferences => {
|
||||||
@ -68,6 +72,10 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences
|
|||||||
download: {
|
download: {
|
||||||
archiveSize: HumanReadableSize.GiB * 4,
|
archiveSize: HumanReadableSize.GiB * 4,
|
||||||
},
|
},
|
||||||
|
purchase: {
|
||||||
|
showSupportBadge: true,
|
||||||
|
hideBuyButtonUntil: new Date(2022, 1, 12).toISOString(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -12,8 +12,9 @@ describe('getKeysDeep', () => {
|
|||||||
foo: 'bar',
|
foo: 'bar',
|
||||||
flag: true,
|
flag: true,
|
||||||
count: 42,
|
count: 42,
|
||||||
|
date: new Date(),
|
||||||
}),
|
}),
|
||||||
).toEqual(['foo', 'flag', 'count']);
|
).toEqual(['foo', 'flag', 'count', 'date']);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should skip undefined properties', () => {
|
it('should skip undefined properties', () => {
|
||||||
|
@ -33,7 +33,7 @@ export const getKeysDeep = (target: unknown, path: string[] = []) => {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_.isObject(value) && !_.isArray(value)) {
|
if (_.isObject(value) && !_.isArray(value) && !_.isDate(value)) {
|
||||||
properties.push(...getKeysDeep(value, [...path, key]));
|
properties.push(...getKeysDeep(value, [...path, key]));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -142,4 +142,46 @@ input:focus-visible {
|
|||||||
.scrollbar-stable {
|
.scrollbar-stable {
|
||||||
scrollbar-gutter: stable both-edges;
|
scrollbar-gutter: stable both-edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Supporter Effect */
|
||||||
|
.supporter-effect {
|
||||||
|
position: relative;
|
||||||
|
border: 0px solid transparent;
|
||||||
|
background-clip: padding-box;
|
||||||
|
animation: gradient 10s ease infinite;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.supporter-effect:hover:after {
|
||||||
|
position: absolute;
|
||||||
|
top: 0px;
|
||||||
|
bottom: 0px;
|
||||||
|
left: 0px;
|
||||||
|
right: 0px;
|
||||||
|
background: linear-gradient(
|
||||||
|
to right,
|
||||||
|
rgba(16, 132, 254, 0.25),
|
||||||
|
rgba(229, 125, 175, 0.25),
|
||||||
|
rgba(254, 36, 29, 0.25),
|
||||||
|
rgba(255, 183, 0, 0.25),
|
||||||
|
rgba(22, 193, 68, 0.25)
|
||||||
|
);
|
||||||
|
content: '';
|
||||||
|
border-radius: 8px;
|
||||||
|
animation: gradient 10s ease infinite;
|
||||||
|
background-size: 400% 400%;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes gradient {
|
||||||
|
0% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
background-position: 100% 50%;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
background-position: 0% 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
export type Type = 'button' | 'submit' | 'reset';
|
export type Type = 'button' | 'submit' | 'reset';
|
||||||
export type Color =
|
export type Color =
|
||||||
| 'primary'
|
| 'primary'
|
||||||
|
| 'primary-inversed'
|
||||||
| 'secondary'
|
| 'secondary'
|
||||||
| 'transparent-primary'
|
| 'transparent-primary'
|
||||||
| 'text-primary'
|
| 'text-primary'
|
||||||
@ -50,6 +51,8 @@
|
|||||||
'dark-gray':
|
'dark-gray':
|
||||||
'dark:border-immich-dark-gray dark:bg-gray-500 enabled:dark:hover:bg-immich-dark-primary/50 enabled:hover:bg-immich-primary/10 dark:text-white',
|
'dark:border-immich-dark-gray dark:bg-gray-500 enabled:dark:hover:bg-immich-dark-primary/50 enabled:hover:bg-immich-primary/10 dark:text-white',
|
||||||
'overlay-primary': 'text-gray-500 enabled:hover:bg-gray-100',
|
'overlay-primary': 'text-gray-500 enabled:hover:bg-gray-100',
|
||||||
|
'primary-inversed':
|
||||||
|
'bg-immich-dark-primary dark:bg-immich-primary text-black dark:text-white enabled:hover:bg-immich-dark-primary/80 enabled:dark:hover:bg-immich-primary/90',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses: Record<Size, string> = {
|
const sizeClasses: Record<Size, string> = {
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
<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>
|
|
@ -1,70 +0,0 @@
|
|||||||
<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>
|
|
@ -1,25 +0,0 @@
|
|||||||
<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>
|
|
@ -1,39 +1,44 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import { ImmichLicense } from '$lib/constants';
|
import { ImmichProduct } from '$lib/constants';
|
||||||
import { getLicenseLink } from '$lib/utils/license-utils';
|
import { getLicenseLink as getProductLink } from '$lib/utils/license-utils';
|
||||||
import { mdiAccount, mdiCheckCircleOutline } from '@mdi/js';
|
import { mdiAccount, mdiCheckCircleOutline } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- USER LICENSE -->
|
<!-- Inidvidual Purchase Option -->
|
||||||
<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="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">
|
<div class="text-immich-primary dark:text-immich-dark-primary">
|
||||||
<Icon path={mdiAccount} size="56" />
|
<Icon path={mdiAccount} size="56" />
|
||||||
<p class="font-semibold text-lg mt-1">{$t('license_individual_title')}</p>
|
<p class="font-semibold text-lg mt-1">{$t('purchase_individual_title')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 dark:text-immich-gray">
|
<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 class="text-6xl font-bold">$25</p>
|
||||||
<p>{$t('license_per_user')}</p>
|
<p>{$t('purchase_per_user')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col justify-between h-[200px] dark:text-immich-gray">
|
<div class="flex flex-col justify-between h-[200px] dark:text-immich-gray">
|
||||||
<div class="mt-6 flex flex-col gap-1">
|
<div class="mt-6 flex flex-col gap-1">
|
||||||
<div class="grid grid-cols-[36px_auto]">
|
<div class="grid grid-cols-[36px_auto]">
|
||||||
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
|
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
|
||||||
<p class="self-center">{$t('license_individual_description_1')}</p>
|
<p class="self-center">{$t('purchase_individual_description_1')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-[36px_auto]">
|
<div class="grid grid-cols-[36px_auto]">
|
||||||
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
|
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
|
||||||
<p class="self-center">{$t('license_lifetime_description')}</p>
|
<p class="self-center">{$t('purchase_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('purchase_individual_description_2')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href={getLicenseLink(ImmichLicense.Client)}>
|
<a href={getProductLink(ImmichProduct.Client)}>
|
||||||
<Button fullwidth>{$t('license_button_select')}</Button>
|
<Button fullwidth>{$t('purchase_button_select')}</Button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -0,0 +1,30 @@
|
|||||||
|
<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';
|
||||||
|
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||||
|
import { preferences } from '$lib/stores/user.store';
|
||||||
|
import { setSupportBadgeVisibility } from '$lib/utils/purchase-utils';
|
||||||
|
|
||||||
|
export let onDone: () => void;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="m-auto w-3/4 text-center flex flex-col place-content-center place-items-center dark:text-white my-6">
|
||||||
|
<Icon path={mdiPartyPopper} class="text-immich-primary dark:text-immich-dark-primary" size="96" />
|
||||||
|
<p class="text-4xl mt-8 font-bold">{$t('purchase_activated_title')}</p>
|
||||||
|
<p class="text-lg mt-6">{$t('purchase_activated_subtitle')}</p>
|
||||||
|
|
||||||
|
<div class="mb-4 w-full mt-6 border rounded-xl p-4 bg-gray-50 dark:bg-gray-900 dark:border-gray-600">
|
||||||
|
<SettingSwitch
|
||||||
|
title={$t('show_supporter_badge')}
|
||||||
|
subtitle={$t('show_supporter_badge_description')}
|
||||||
|
bind:checked={$preferences.purchase.showSupportBadge}
|
||||||
|
on:toggle={({ detail }) => setSupportBadgeVisibility(detail)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 w-full">
|
||||||
|
<Button fullwidth on:click={onDone}>OK</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,84 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import ServerPurchaseOptionCard from './server-purchase-option-card.svelte';
|
||||||
|
import UserPurchaseOptionCard from './individual-purchase-option-card.svelte';
|
||||||
|
import { activateProduct, 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 { purchaseStore } from '$lib/stores/purchase.store';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
export let onActivate: () => void;
|
||||||
|
|
||||||
|
export let showTitle = true;
|
||||||
|
export let showMessage = true;
|
||||||
|
let productKey = '';
|
||||||
|
let isLoading = false;
|
||||||
|
|
||||||
|
const activate = async () => {
|
||||||
|
try {
|
||||||
|
productKey = productKey.trim();
|
||||||
|
isLoading = true;
|
||||||
|
|
||||||
|
const activationKey = await getActivationKey(productKey);
|
||||||
|
await activateProduct(productKey, activationKey);
|
||||||
|
|
||||||
|
onActivate();
|
||||||
|
purchaseStore.setPurchaseStatus(true);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, $t('purchase_failed_activation'));
|
||||||
|
} finally {
|
||||||
|
isLoading = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="p-4">
|
||||||
|
<div>
|
||||||
|
{#if showTitle}
|
||||||
|
<h1 class="text-4xl font-bold text-immich-primary dark:text-immich-dark-primary tracking-wider">
|
||||||
|
{$t('purchase_option_title')}
|
||||||
|
</h1>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if showMessage}
|
||||||
|
<div class="mt-2 dark:text-immich-gray">
|
||||||
|
<p>
|
||||||
|
{$t('purchase_panel_info_1')}
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
{$t('purchase_panel_info_2')}
|
||||||
|
</p>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="flex gap-6 mt-4 justify-between">
|
||||||
|
<ServerPurchaseOptionCard />
|
||||||
|
<UserPurchaseOptionCard />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6">
|
||||||
|
<p class="dark:text-immich-gray">{$t('purchase_input_suggestion')}</p>
|
||||||
|
<form class="mt-2 flex gap-2" on:submit={activate}>
|
||||||
|
<input
|
||||||
|
class="immich-form-input w-full"
|
||||||
|
id="purchaseKey"
|
||||||
|
type="text"
|
||||||
|
bind:value={productKey}
|
||||||
|
required
|
||||||
|
placeholder="IMCL-0KEY-0CAN-00BE-FOUD-FROM-YOUR-EMAIL-INBX"
|
||||||
|
disabled={isLoading}
|
||||||
|
/>
|
||||||
|
<Button type="submit" rounded="lg"
|
||||||
|
>{#if isLoading}
|
||||||
|
<LoadingSpinner />
|
||||||
|
{:else}
|
||||||
|
{$t('purchase_button_activate')}
|
||||||
|
{/if}</Button
|
||||||
|
>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
@ -0,0 +1,26 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
|
||||||
|
import PurchaseActivationSuccess from '$lib/components/shared-components/purchasing/purchase-activation-success.svelte';
|
||||||
|
import PurchaseContent from '$lib/components/shared-components/purchasing/purchase-content.svelte';
|
||||||
|
|
||||||
|
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
||||||
|
|
||||||
|
export let onClose: () => void;
|
||||||
|
|
||||||
|
let showProductActivated = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Portal>
|
||||||
|
<FullScreenModal showLogo title={''} {onClose} width="wide">
|
||||||
|
{#if showProductActivated}
|
||||||
|
<PurchaseActivationSuccess onDone={onClose} />
|
||||||
|
{:else}
|
||||||
|
<PurchaseContent
|
||||||
|
onActivate={() => {
|
||||||
|
showProductActivated = true;
|
||||||
|
}}
|
||||||
|
showMessage={false}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
</FullScreenModal>
|
||||||
|
</Portal>
|
@ -1,44 +1,44 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import { ImmichLicense } from '$lib/constants';
|
import { ImmichProduct } from '$lib/constants';
|
||||||
import { getLicenseLink } from '$lib/utils/license-utils';
|
import { getLicenseLink } from '$lib/utils/license-utils';
|
||||||
import { mdiCheckCircleOutline, mdiServer } from '@mdi/js';
|
import { mdiCheckCircleOutline, mdiServer } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- SERVER LICENSE -->
|
<!-- SERVER Purchase Options -->
|
||||||
<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="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">
|
<div class="text-immich-primary dark:text-immich-dark-primary">
|
||||||
<Icon path={mdiServer} size="56" />
|
<Icon path={mdiServer} size="56" />
|
||||||
<p class="font-semibold text-lg mt-1">{$t('license_server_title')}</p>
|
<p class="font-semibold text-lg mt-1">{$t('purchase_server_title')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4 dark:text-immich-gray">
|
<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 class="text-6xl font-bold">$100</p>
|
||||||
<p>{$t('license_per_server')}</p>
|
<p>{$t('purchase_per_server')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-col justify-between h-[200px] dark:text-immich-gray">
|
<div class="flex flex-col justify-between h-[200px] dark:text-immich-gray">
|
||||||
<div class="mt-6 flex flex-col gap-1">
|
<div class="mt-6 flex flex-col gap-1">
|
||||||
<div class="grid grid-cols-[36px_auto]">
|
<div class="grid grid-cols-[36px_auto]">
|
||||||
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
|
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
|
||||||
<p class="self-center">{$t('license_server_description_1')}</p>
|
<p class="self-center">{$t('purchase_server_description_1')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-[36px_auto]">
|
<div class="grid grid-cols-[36px_auto]">
|
||||||
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
|
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
|
||||||
<p class="self-center">{$t('license_lifetime_description')}</p>
|
<p class="self-center">{$t('purchase_lifetime_description')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="grid grid-cols-[36px_auto]">
|
<div class="grid grid-cols-[36px_auto]">
|
||||||
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
|
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
|
||||||
<p class="self-center">{$t('license_server_description_2')}</p>
|
<p class="self-center">{$t('purchase_server_description_2')}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a href={getLicenseLink(ImmichLicense.Server)}>
|
<a href={getLicenseLink(ImmichProduct.Server)}>
|
||||||
<Button fullwidth>{$t('license_button_select')}</Button>
|
<Button fullwidth>{$t('purchase_button_select')}</Button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
@ -10,11 +10,20 @@
|
|||||||
export let key: string;
|
export let key: string;
|
||||||
export let isOpen = $accordionState.has(key);
|
export let isOpen = $accordionState.has(key);
|
||||||
|
|
||||||
|
let accordionElement: HTMLDivElement;
|
||||||
|
|
||||||
$: setIsOpen(isOpen);
|
$: setIsOpen(isOpen);
|
||||||
|
|
||||||
const setIsOpen = (isOpen: boolean) => {
|
const setIsOpen = (isOpen: boolean) => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
$accordionState = $accordionState.add(key);
|
$accordionState = $accordionState.add(key);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
accordionElement.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start',
|
||||||
|
});
|
||||||
|
}, 200);
|
||||||
} else {
|
} else {
|
||||||
$accordionState.delete(key);
|
$accordionState.delete(key);
|
||||||
$accordionState = $accordionState;
|
$accordionState = $accordionState;
|
||||||
@ -26,7 +35,7 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="border-b-[1px] border-gray-200 py-4 dark:border-gray-700">
|
<div class="border-b-[1px] border-gray-200 py-4 dark:border-gray-700" bind:this={accordionElement}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import LicenseInfo from './license-info.svelte';
|
import PurchaseInfo from './purchase-info.svelte';
|
||||||
import ServerStatus from './server-status.svelte';
|
import ServerStatus from './server-status.svelte';
|
||||||
import StorageSpace from './storage-space.svelte';
|
import StorageSpace from './storage-space.svelte';
|
||||||
</script>
|
</script>
|
||||||
@ -8,10 +8,8 @@
|
|||||||
<StorageSpace />
|
<StorageSpace />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-2">
|
<PurchaseInfo />
|
||||||
<LicenseInfo />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6 mt-2">
|
||||||
<ServerStatus />
|
<ServerStatus />
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,117 +0,0 @@
|
|||||||
<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';
|
|
||||||
import { fade } from 'svelte/transition';
|
|
||||||
|
|
||||||
let showMessage = false;
|
|
||||||
let isOpen = false;
|
|
||||||
let hoverMessage = false;
|
|
||||||
let hoverButton = false;
|
|
||||||
const { isLicenseActivated } = licenseStore;
|
|
||||||
|
|
||||||
const openLicenseModal = () => {
|
|
||||||
isOpen = true;
|
|
||||||
showMessage = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onButtonHover = () => {
|
|
||||||
showMessage = true;
|
|
||||||
hoverButton = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
$: if (showMessage && !hoverMessage && !hoverButton) {
|
|
||||||
setTimeout(() => {
|
|
||||||
if (!hoverMessage && !hoverButton) {
|
|
||||||
showMessage = false;
|
|
||||||
}
|
|
||||||
}, 300);
|
|
||||||
}
|
|
||||||
</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:mouseover={onButtonHover}
|
|
||||||
on:mouseleave={() => (hoverButton = false)}
|
|
||||||
on:focus={onButtonHover}
|
|
||||||
on:blur={() => (hoverButton = false)}
|
|
||||||
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-64 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"
|
|
||||||
transition:fade={{ duration: 150 }}
|
|
||||||
on:mouseover={() => (hoverMessage = true)}
|
|
||||||
on:mouseleave={() => (hoverMessage = false)}
|
|
||||||
on:focus={() => (hoverMessage = true)}
|
|
||||||
on:blur={() => (hoverMessage = false)}
|
|
||||||
role="dialog"
|
|
||||||
>
|
|
||||||
<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={$t('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,179 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
|
import { mdiClose, mdiInformationOutline } 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/purchasing/purchase-modal.svelte';
|
||||||
|
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
import { AppRoute } from '$lib/constants';
|
||||||
|
import { getAccountAge } from '$lib/utils/auth';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
|
||||||
|
import { updateMyPreferences } from '@immich/sdk';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { preferences } from '$lib/stores/user.store';
|
||||||
|
import { getButtonVisibility } from '$lib/utils/purchase-utils';
|
||||||
|
|
||||||
|
let showMessage = false;
|
||||||
|
let isOpen = false;
|
||||||
|
let hoverMessage = false;
|
||||||
|
let hoverButton = false;
|
||||||
|
|
||||||
|
let showBuyButton = getButtonVisibility();
|
||||||
|
|
||||||
|
const { isPurchased } = purchaseStore;
|
||||||
|
|
||||||
|
const openPurchaseModal = () => {
|
||||||
|
isOpen = true;
|
||||||
|
showMessage = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onButtonHover = () => {
|
||||||
|
showMessage = true;
|
||||||
|
hoverButton = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const hideButton = async (always: boolean) => {
|
||||||
|
const hideBuyButtonUntil = new Date();
|
||||||
|
|
||||||
|
if (always) {
|
||||||
|
hideBuyButtonUntil.setFullYear(2124); // see ya in 100 years
|
||||||
|
} else {
|
||||||
|
hideBuyButtonUntil.setDate(hideBuyButtonUntil.getDate() + 30);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await updateMyPreferences({
|
||||||
|
userPreferencesUpdateDto: {
|
||||||
|
purchase: {
|
||||||
|
hideBuyButtonUntil: hideBuyButtonUntil.toISOString(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
preferences.set(response);
|
||||||
|
showBuyButton = getButtonVisibility();
|
||||||
|
showMessage = false;
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, 'Error hiding buy button');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$: if (showMessage && !hoverMessage && !hoverButton) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!hoverMessage && !hoverButton) {
|
||||||
|
showMessage = false;
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if isOpen}
|
||||||
|
<LicenseModal onClose={() => (isOpen = false)} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if getAccountAge() > 14}
|
||||||
|
<div class="hidden md:block license-status pl-4 text-sm">
|
||||||
|
{#if $isPurchased && $preferences.purchase.showSupportBadge}
|
||||||
|
<button
|
||||||
|
on:click={() => goto(`${AppRoute.USER_SETTINGS}?isOpen=user-purchase-settings`)}
|
||||||
|
class="w-full"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex gap-1 mt-2 place-items-center dark:bg-immich-dark-primary/10 bg-gray-200/50 p-2 border supporter-effect rounded-lg transition-all"
|
||||||
|
>
|
||||||
|
<div class="h-6 w-6">
|
||||||
|
<ImmichLogo noText />
|
||||||
|
</div>
|
||||||
|
<p class="dark:text-gray-100">Supporter</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{:else if !$isPurchased && showBuyButton}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
on:click={openPurchaseModal}
|
||||||
|
on:mouseover={onButtonHover}
|
||||||
|
on:mouseleave={() => (hoverButton = false)}
|
||||||
|
on:focus={onButtonHover}
|
||||||
|
on:blur={() => (hoverButton = false)}
|
||||||
|
class="p-2 flex justify-between place-items-center place-content-center border border-immich-primary/20 dark:border-immich-dark-primary/10 mt-2 rounded-lg shadow-md dark:bg-immich-dark-primary/10 w-full"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between w-full place-items-center place-content-center">
|
||||||
|
<div class="flex place-items-center place-content-center gap-1">
|
||||||
|
<div class="h-6 w-6">
|
||||||
|
<ImmichLogo noText />
|
||||||
|
</div>
|
||||||
|
<p class="flex text-immich-primary dark:text-immich-dark-primary font-medium">
|
||||||
|
{$t('purchase_button_buy_immich')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Icon
|
||||||
|
path={mdiInformationOutline}
|
||||||
|
class="flex text-immich-primary dark:text-immich-dark-primary font-medium"
|
||||||
|
size="18"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<Portal target="body">
|
||||||
|
{#if showMessage}
|
||||||
|
<div
|
||||||
|
class="w-[500px] absolute bottom-[75px] left-[255px] bg-gray-50 dark:border-gray-800 border border-gray-200 dark:bg-immich-dark-gray dark:text-white text-black rounded-3xl z-10 shadow-2xl px-8 py-6"
|
||||||
|
transition:fade={{ duration: 150 }}
|
||||||
|
on:mouseover={() => (hoverMessage = true)}
|
||||||
|
on:mouseleave={() => (hoverMessage = false)}
|
||||||
|
on:focus={() => (hoverMessage = true)}
|
||||||
|
on:blur={() => (hoverMessage = false)}
|
||||||
|
role="dialog"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between place-items-center">
|
||||||
|
<div class="h-10 w-10">
|
||||||
|
<ImmichLogo noText />
|
||||||
|
</div>
|
||||||
|
<CircleIconButton
|
||||||
|
icon={mdiClose}
|
||||||
|
on:click={() => {
|
||||||
|
showMessage = false;
|
||||||
|
}}
|
||||||
|
title={$t('close')}
|
||||||
|
size="18"
|
||||||
|
class="text-immich-dark-gray/85 dark:text-immich-gray"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="text-lg font-medium my-3 dark:text-immich-dark-primary text-immich-primary">
|
||||||
|
{$t('purchase_panel_title')}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="text-gray-800 dark:text-white my-4">
|
||||||
|
<p>
|
||||||
|
{$t('purchase_panel_info_1')}
|
||||||
|
</p>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
{$t('purchase_panel_info_2')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button class="mt-2" fullwidth on:click={openPurchaseModal}>{$t('purchase_button_buy_immich')}</Button>
|
||||||
|
<div class="mt-3 flex gap-4">
|
||||||
|
<Button size="sm" fullwidth shadow={false} color="transparent-gray" on:click={() => hideButton(true)}>
|
||||||
|
{$t('purchase_button_never_show_again')}
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" fullwidth shadow={false} color="transparent-gray" on:click={() => hideButton(false)}>
|
||||||
|
{$t('purchase_button_reminder')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Portal>
|
@ -1,172 +0,0 @@
|
|||||||
<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,
|
|
||||||
isHttpError,
|
|
||||||
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 getServerLicenseInfo();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getServerLicenseInfo = async () => {
|
|
||||||
try {
|
|
||||||
return await getServerLicense();
|
|
||||||
} catch (error) {
|
|
||||||
if (isHttpError(error) && error.status === 404) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
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>
|
|
||||||
|
|
||||||
{#if $user.isAdmin}
|
|
||||||
<div class="text-right mt-4">
|
|
||||||
<Button size="sm" color="red" on:click={removeServerLicense}>Remove license</Button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{: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>
|
|
@ -0,0 +1,180 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||||
|
import { preferences, user } from '$lib/stores/user.store';
|
||||||
|
import {
|
||||||
|
deleteServerLicense as deleteServerProductKey,
|
||||||
|
deleteUserLicense as deleteIndividualProductKey,
|
||||||
|
getAboutInfo,
|
||||||
|
getMyUser,
|
||||||
|
getServerLicense,
|
||||||
|
isHttpError,
|
||||||
|
type LicenseResponseDto,
|
||||||
|
} from '@immich/sdk';
|
||||||
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
|
import { mdiKey } 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 PurchaseContent from '$lib/components/shared-components/purchasing/purchase-content.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||||
|
import { setSupportBadgeVisibility } from '$lib/utils/purchase-utils';
|
||||||
|
const { isPurchased } = purchaseStore;
|
||||||
|
|
||||||
|
let isServerProduct = false;
|
||||||
|
let serverPurchaseInfo: LicenseResponseDto | null = null;
|
||||||
|
|
||||||
|
const checkPurchaseInfo = async () => {
|
||||||
|
const serverInfo = await getAboutInfo();
|
||||||
|
isServerProduct = serverInfo.licensed;
|
||||||
|
|
||||||
|
const userInfo = await getMyUser();
|
||||||
|
if (userInfo.license) {
|
||||||
|
$user = { ...$user, license: userInfo.license };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isServerProduct && $user.isAdmin) {
|
||||||
|
serverPurchaseInfo = await getServerPurchaseInfo();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getServerPurchaseInfo = async () => {
|
||||||
|
try {
|
||||||
|
return await getServerLicense();
|
||||||
|
} catch (error) {
|
||||||
|
if (isHttpError(error) && error.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (!$isPurchased) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkPurchaseInfo();
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeIndividualProductKey = async () => {
|
||||||
|
try {
|
||||||
|
const isConfirmed = await dialogController.show({
|
||||||
|
title: 'Remove Product Key',
|
||||||
|
prompt: 'Are you sure you want to remove the product key?',
|
||||||
|
confirmText: 'Remove',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isConfirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteIndividualProductKey();
|
||||||
|
purchaseStore.setPurchaseStatus(false);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, 'Failed to remove product key');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeServerProductKey = async () => {
|
||||||
|
try {
|
||||||
|
const isConfirmed = await dialogController.show({
|
||||||
|
title: 'Remove License',
|
||||||
|
prompt: 'Are you sure you want to remove the Server product key?',
|
||||||
|
confirmText: 'Remove',
|
||||||
|
cancelText: 'Cancel',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isConfirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteServerProductKey();
|
||||||
|
purchaseStore.setPurchaseStatus(false);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(error, 'Failed to remove product key');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onProductActivated = async () => {
|
||||||
|
purchaseStore.setPurchaseStatus(true);
|
||||||
|
await checkPurchaseInfo();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<section class="my-4">
|
||||||
|
<div in:fade={{ duration: 500 }}>
|
||||||
|
{#if $isPurchased}
|
||||||
|
<!-- BADGE TOGGLE -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<SettingSwitch
|
||||||
|
title={$t('show_supporter_badge')}
|
||||||
|
subtitle={$t('show_supporter_badge_description')}
|
||||||
|
bind:checked={$preferences.purchase.showSupportBadge}
|
||||||
|
on:toggle={({ detail }) => setSupportBadgeVisibility(detail)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- PRODUCT KEY INFO CARD -->
|
||||||
|
{#if isServerProduct}
|
||||||
|
<div
|
||||||
|
class="bg-gray-50 border border-immich-dark-primary/20 dark:bg-immich-dark-primary/15 p-6 pr-12 rounded-xl flex place-content-center gap-4"
|
||||||
|
>
|
||||||
|
<Icon path={mdiKey} 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">
|
||||||
|
{$t('purchase_server_title')}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{#if $user.isAdmin && serverPurchaseInfo?.activatedAt}
|
||||||
|
<p class="dark:text-white text-sm mt-1 col-start-2">
|
||||||
|
{$t('purchase_activated_time', {
|
||||||
|
values: { date: new Date(serverPurchaseInfo.activatedAt).toLocaleDateString() },
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
{:else}
|
||||||
|
<p class="dark:text-white">{$t('purchase_settings_server_activated')}</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $user.isAdmin}
|
||||||
|
<div class="text-right mt-4">
|
||||||
|
<Button size="sm" color="red" on:click={removeServerProductKey}>{$t('purchase_button_remove_key')}</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<div
|
||||||
|
class="bg-gray-50 border border-immich-dark-primary/20 dark:bg-immich-dark-primary/15 p-6 pr-12 rounded-xl flex place-content-center gap-4"
|
||||||
|
>
|
||||||
|
<Icon path={mdiKey} 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">
|
||||||
|
{$t('purchase_individual_title')}
|
||||||
|
</p>
|
||||||
|
{#if $user.license?.activatedAt}
|
||||||
|
<p class="dark:text-white text-sm mt-1 col-start-2">
|
||||||
|
{$t('purchase_activated_time', {
|
||||||
|
values: { date: new Date($user.license?.activatedAt).toLocaleDateString() },
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-right mt-4">
|
||||||
|
<Button size="sm" color="red" on:click={removeIndividualProductKey}>{$t('purchase_button_remove_key')}</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<PurchaseContent onActivate={onProductActivated} showTitle={false} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</section>
|
@ -18,7 +18,7 @@
|
|||||||
import NotificationsSettings from '$lib/components/user-settings-page/notifications-settings.svelte';
|
import NotificationsSettings from '$lib/components/user-settings-page/notifications-settings.svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import DownloadSettings from '$lib/components/user-settings-page/download-settings.svelte';
|
import DownloadSettings from '$lib/components/user-settings-page/download-settings.svelte';
|
||||||
import LicenseSettings from '$lib/components/user-settings-page/license-settings.svelte';
|
import UserPurchaseSettings from '$lib/components/user-settings-page/user-purchase-settings.svelte';
|
||||||
|
|
||||||
export let keys: ApiKeyResponseDto[] = [];
|
export let keys: ApiKeyResponseDto[] = [];
|
||||||
export let sessions: SessionResponseDto[] = [];
|
export let sessions: SessionResponseDto[] = [];
|
||||||
@ -53,14 +53,6 @@
|
|||||||
<DownloadSettings />
|
<DownloadSettings />
|
||||||
</SettingAccordion>
|
</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')}>
|
<SettingAccordion key="memories" title={$t('memories')} subtitle={$t('memories_setting_description')}>
|
||||||
<MemoriesSettings />
|
<MemoriesSettings />
|
||||||
</SettingAccordion>
|
</SettingAccordion>
|
||||||
@ -87,4 +79,12 @@
|
|||||||
<SettingAccordion key="partner-sharing" title={$t('partner_sharing')} subtitle={$t('manage_sharing_with_partners')}>
|
<SettingAccordion key="partner-sharing" title={$t('partner_sharing')} subtitle={$t('manage_sharing_with_partners')}>
|
||||||
<PartnerSettings user={$user} />
|
<PartnerSettings user={$user} />
|
||||||
</SettingAccordion>
|
</SettingAccordion>
|
||||||
|
|
||||||
|
<SettingAccordion
|
||||||
|
key="user-purchase-settings"
|
||||||
|
title={$t('user_purchase_settings')}
|
||||||
|
subtitle={$t('user_purchase_settings_description')}
|
||||||
|
>
|
||||||
|
<UserPurchaseSettings />
|
||||||
|
</SettingAccordion>
|
||||||
</SettingAccordionState>
|
</SettingAccordionState>
|
||||||
|
@ -311,7 +311,7 @@ export const langs = [
|
|||||||
{ name: 'Development (keys only)', code: 'dev', loader: () => Promise.resolve({}) },
|
{ name: 'Development (keys only)', code: 'dev', loader: () => Promise.resolve({}) },
|
||||||
];
|
];
|
||||||
|
|
||||||
export enum ImmichLicense {
|
export enum ImmichProduct {
|
||||||
Client = 'immich-client',
|
Client = 'immich-client',
|
||||||
Server = 'immich-server',
|
Server = 'immich-server',
|
||||||
}
|
}
|
||||||
|
@ -405,7 +405,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_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_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.",
|
"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",
|
"buy": "Purchase Immich",
|
||||||
"camera": "Camera",
|
"camera": "Camera",
|
||||||
"camera_brand": "Camera brand",
|
"camera_brand": "Camera brand",
|
||||||
"camera_model": "Camera model",
|
"camera_model": "Camera model",
|
||||||
@ -747,31 +747,6 @@
|
|||||||
"level": "Level",
|
"level": "Level",
|
||||||
"library": "Library",
|
"library": "Library",
|
||||||
"library_options": "Library options",
|
"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 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 consider purchasing a license to support the continued development of the service",
|
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
"like_deleted": "Like deleted",
|
"like_deleted": "Like deleted",
|
||||||
"link_options": "Link options",
|
"link_options": "Link options",
|
||||||
@ -939,6 +914,34 @@
|
|||||||
"profile_picture_set": "Profile picture set.",
|
"profile_picture_set": "Profile picture set.",
|
||||||
"public_album": "Public album",
|
"public_album": "Public album",
|
||||||
"public_share": "Public Share",
|
"public_share": "Public Share",
|
||||||
|
"purchase_account_info": "Supporter",
|
||||||
|
"purchase_activated_subtitle": "Thank you for supporting Immich and open-source software",
|
||||||
|
"purchase_activated_time": "Activated on {date}",
|
||||||
|
"purchase_activated_title": "Your key has been successfully activated",
|
||||||
|
"purchase_button_activate": "Activate",
|
||||||
|
"purchase_button_buy": "Buy",
|
||||||
|
"purchase_button_buy_immich": "Buy Immich",
|
||||||
|
"purchase_button_never_show_again": "Never show again",
|
||||||
|
"purchase_button_reminder": "Remind me in 30 days",
|
||||||
|
"purchase_button_remove_key": "Remove key",
|
||||||
|
"purchase_button_select": "Select",
|
||||||
|
"purchase_failed_activation": "Failed to activate! Please check your email for the the correct product key!",
|
||||||
|
"purchase_individual_description_1": "For an individual",
|
||||||
|
"purchase_individual_description_2": "Supporter status",
|
||||||
|
"purchase_individual_title": "Individual",
|
||||||
|
"purchase_input_suggestion": "Have a product key? Enter the key below",
|
||||||
|
"purchase_license_subtitle": "Buy Immich to support the continued development of the service",
|
||||||
|
"purchase_lifetime_description": "Lifetime purchase",
|
||||||
|
"purchase_option_title": "PURCHASE OPTIONS",
|
||||||
|
"purchase_panel_info_1": "Building Immich takes a lot of time and effort, and we have full-time engineers working on it to make it as good as we possibly can. Our mission is for open-source software and ethical business practices to become a sustainable income source for developers and to create a privacy-respecting ecosystem with real alternatives to exploitative cloud services.",
|
||||||
|
"purchase_panel_info_2": "As we’re committed not to add paywalls, this purchase will not grant you any additional features in Immich. We rely on users like you to support Immich’s ongoing development.",
|
||||||
|
"purchase_panel_title": "Support the project",
|
||||||
|
"purchase_per_server": "Per server",
|
||||||
|
"purchase_per_user": "Per user",
|
||||||
|
"purchase_server_description_1": "For the whole server",
|
||||||
|
"purchase_server_description_2": "Supporter status",
|
||||||
|
"purchase_server_title": "Server",
|
||||||
|
"purchase_settings_server_activated": "The server product key is managed by the admin",
|
||||||
"reaction_options": "Reaction options",
|
"reaction_options": "Reaction options",
|
||||||
"read_changelog": "Read Changelog",
|
"read_changelog": "Read Changelog",
|
||||||
"reassign": "Reassign",
|
"reassign": "Reassign",
|
||||||
@ -1078,6 +1081,8 @@
|
|||||||
"show_person_options": "Show person options",
|
"show_person_options": "Show person options",
|
||||||
"show_progress_bar": "Show Progress Bar",
|
"show_progress_bar": "Show Progress Bar",
|
||||||
"show_search_options": "Show search options",
|
"show_search_options": "Show search options",
|
||||||
|
"show_supporter_badge": "Supporter badge",
|
||||||
|
"show_supporter_badge_description": "Show a supporter badge",
|
||||||
"shuffle": "Shuffle",
|
"shuffle": "Shuffle",
|
||||||
"sign_out": "Sign Out",
|
"sign_out": "Sign Out",
|
||||||
"sign_up": "Sign up",
|
"sign_up": "Sign up",
|
||||||
@ -1168,9 +1173,9 @@
|
|||||||
"use_custom_date_range": "Use custom date range instead",
|
"use_custom_date_range": "Use custom date range instead",
|
||||||
"user": "User",
|
"user": "User",
|
||||||
"user_id": "User ID",
|
"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_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}",
|
||||||
|
"user_purchase_settings": "Purchase",
|
||||||
|
"user_purchase_settings_description": "Manage your purchase",
|
||||||
"user_role_set": "Set {user} as {role}",
|
"user_role_set": "Set {user} as {role}",
|
||||||
"user_usage_detail": "User usage detail",
|
"user_usage_detail": "User usage detail",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
|
@ -1,18 +0,0 @@
|
|||||||
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();
|
|
18
web/src/lib/stores/purchase.store.ts
Normal file
18
web/src/lib/stores/purchase.store.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
function createPurchaseStore() {
|
||||||
|
const isPurcharsed = writable(false);
|
||||||
|
|
||||||
|
function setPurchaseStatus(status: boolean) {
|
||||||
|
isPurcharsed.set(status);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isPurchased: {
|
||||||
|
subscribe: isPurcharsed.subscribe,
|
||||||
|
},
|
||||||
|
setPurchaseStatus,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const purchaseStore = createPurchaseStore();
|
@ -1,4 +1,4 @@
|
|||||||
import { licenseStore } from '$lib/stores/license.store';
|
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||||
import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk';
|
import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
@ -12,5 +12,5 @@ export const preferences = writable<UserPreferencesResponseDto>();
|
|||||||
export const resetSavedUser = () => {
|
export const resetSavedUser = () => {
|
||||||
user.set(undefined as unknown as UserAdminResponseDto);
|
user.set(undefined as unknown as UserAdminResponseDto);
|
||||||
preferences.set(undefined as unknown as UserPreferencesResponseDto);
|
preferences.set(undefined as unknown as UserPreferencesResponseDto);
|
||||||
licenseStore.setLicenseStatus(false);
|
purchaseStore.setPurchaseStatus(false);
|
||||||
};
|
};
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import { licenseStore } from '$lib/stores/license.store';
|
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||||
import { serverInfo } from '$lib/stores/server-info.store';
|
import { serverInfo } from '$lib/stores/server-info.store';
|
||||||
import { preferences as preferences$, user as user$ } from '$lib/stores/user.store';
|
import { preferences as preferences$, user as user$ } from '$lib/stores/user.store';
|
||||||
import { getAboutInfo, getMyPreferences, getMyUser, getStorage } from '@immich/sdk';
|
import { getAboutInfo, getMyPreferences, getMyUser, getStorage } from '@immich/sdk';
|
||||||
@ -26,7 +26,7 @@ export const loadUser = async () => {
|
|||||||
|
|
||||||
// Check for license status
|
// Check for license status
|
||||||
if (serverInfo.licensed || user.license?.activatedAt) {
|
if (serverInfo.licensed || user.license?.activatedAt) {
|
||||||
licenseStore.setLicenseStatus(true);
|
purchaseStore.setPurchaseStatus(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return user;
|
return user;
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { PUBLIC_IMMICH_BUY_HOST, PUBLIC_IMMICH_PAY_HOST } from '$env/static/public';
|
import { PUBLIC_IMMICH_BUY_HOST, PUBLIC_IMMICH_PAY_HOST } from '$env/static/public';
|
||||||
import type { ImmichLicense } from '$lib/constants';
|
import type { ImmichProduct } from '$lib/constants';
|
||||||
import { serverConfig } from '$lib/stores/server-config.store';
|
import { serverConfig } from '$lib/stores/server-config.store';
|
||||||
import { setServerLicense, setUserLicense, type LicenseResponseDto } from '@immich/sdk';
|
import { setServerLicense, setUserLicense, type LicenseResponseDto } from '@immich/sdk';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { loadUser } from './auth';
|
import { loadUser } from './auth';
|
||||||
|
|
||||||
export const activateLicense = async (licenseKey: string, activationKey: string): Promise<LicenseResponseDto> => {
|
export const activateProduct = async (licenseKey: string, activationKey: string): Promise<LicenseResponseDto> => {
|
||||||
// Send server key to user activation if user is not admin
|
// Send server key to user activation if user is not admin
|
||||||
const user = await loadUser();
|
const user = await loadUser();
|
||||||
const isServerActivation = user?.isAdmin && licenseKey.search('IMSV') !== -1;
|
const isServerActivation = user?.isAdmin && licenseKey.search('IMSV') !== -1;
|
||||||
@ -21,7 +21,7 @@ export const getActivationKey = async (licenseKey: string): Promise<string> => {
|
|||||||
return response.text();
|
return response.text();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getLicenseLink = (license: ImmichLicense) => {
|
export const getLicenseLink = (license: ImmichProduct) => {
|
||||||
const url = new URL('/', PUBLIC_IMMICH_BUY_HOST);
|
const url = new URL('/', PUBLIC_IMMICH_BUY_HOST);
|
||||||
url.searchParams.append('productId', license);
|
url.searchParams.append('productId', license);
|
||||||
url.searchParams.append('instanceUrl', get(serverConfig).externalDomain || window.origin);
|
url.searchParams.append('instanceUrl', get(serverConfig).externalDomain || window.origin);
|
||||||
|
32
web/src/lib/utils/purchase-utils.ts
Normal file
32
web/src/lib/utils/purchase-utils.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { preferences } from '$lib/stores/user.store';
|
||||||
|
import { updateMyPreferences } from '@immich/sdk';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
|
export const getButtonVisibility = (): boolean => {
|
||||||
|
const myPreferences = get(preferences);
|
||||||
|
|
||||||
|
if (!myPreferences) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { purchase } = myPreferences;
|
||||||
|
|
||||||
|
const now = DateTime.now();
|
||||||
|
const hideUntilDate = DateTime.fromISO(purchase.hideBuyButtonUntil);
|
||||||
|
const dayLeft = Number(now.diff(hideUntilDate, 'days').days.toFixed(0));
|
||||||
|
|
||||||
|
return dayLeft > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setSupportBadgeVisibility = async (value: boolean) => {
|
||||||
|
const response = await updateMyPreferences({
|
||||||
|
userPreferencesUpdateDto: {
|
||||||
|
purchase: {
|
||||||
|
showSupportBadge: value,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
preferences.set(response);
|
||||||
|
};
|
@ -1,41 +1,42 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||||
import LicenseActivationSuccess from '$lib/components/shared-components/license/license-activation-success.svelte';
|
import LicenseActivationSuccess from '$lib/components/shared-components/purchasing/purchase-activation-success.svelte';
|
||||||
import LicenseContent from '$lib/components/shared-components/license/license-content.svelte';
|
import LicenseContent from '$lib/components/shared-components/purchasing/purchase-content.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { user } from '$lib/stores/user.store';
|
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import { mdiAlertCircleOutline, mdiLicense } from '@mdi/js';
|
import { mdiAlertCircleOutline } from '@mdi/js';
|
||||||
import { licenseStore } from '$lib/stores/license.store';
|
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||||
|
import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
let showLicenseActivated = false;
|
let showLicenseActivated = false;
|
||||||
const { isLicenseActivated } = licenseStore;
|
const { isPurchased } = purchaseStore;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<UserPageLayout title={$t('buy')}>
|
<UserPageLayout title={$t('buy')}>
|
||||||
<section class="mx-4 flex place-content-center">
|
<section class="mx-4 flex place-content-center">
|
||||||
<div class={`w-full ${$user.isAdmin ? 'max-w-3xl' : 'max-w-xl'}`}>
|
<div class="w-full max-w-3xl">
|
||||||
{#if data.isActivated === false}
|
{#if data.isActivated === false}
|
||||||
<div
|
<div
|
||||||
class="bg-red-100 text-red-700 px-4 py-3 rounded-md flex place-items-center place-content-center gap-2"
|
class="bg-red-100 text-red-700 px-4 py-3 rounded-md flex place-items-center place-content-center gap-2"
|
||||||
role="alert"
|
role="alert"
|
||||||
>
|
>
|
||||||
<Icon path={mdiAlertCircleOutline} size="18" />
|
<Icon path={mdiAlertCircleOutline} size="18" />
|
||||||
<p>{$t('license_failed_activation')}</p>
|
<p>{$t('purchase_failed_activation')}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $isLicenseActivated}
|
{#if $isPurchased}
|
||||||
<div
|
<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"
|
class="flex gap-1 mt-2 place-items-center place-content-center dark:bg-immich-dark-primary/10 bg-gray-200/50 p-2 border rounded-lg dark:text-white supporter-effect"
|
||||||
role="alert"
|
|
||||||
>
|
>
|
||||||
<Icon path={mdiLicense} size="24" />
|
<div class="h-8 w-8">
|
||||||
<p>{$t('license_account_info')}</p>
|
<ImmichLogo noText />
|
||||||
|
</div>
|
||||||
|
<p>{$t('purchase_account_info')}</p>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { licenseStore } from '$lib/stores/license.store';
|
import { purchaseStore } from '$lib/stores/purchase.store';
|
||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { activateLicense, getActivationKey } from '$lib/utils/license-utils';
|
import { activateProduct, getActivationKey } from '$lib/utils/license-utils';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ url }) => {
|
export const load = (async ({ url }) => {
|
||||||
@ -18,10 +18,10 @@ export const load = (async ({ url }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (licenseKey && activationKey) {
|
if (licenseKey && activationKey) {
|
||||||
const response = await activateLicense(licenseKey, activationKey);
|
const response = await activateProduct(licenseKey, activationKey);
|
||||||
if (response.activatedAt !== '') {
|
if (response.activatedAt !== '') {
|
||||||
isActivated = true;
|
isActivated = true;
|
||||||
licenseStore.setLicenseStatus(true);
|
purchaseStore.setPurchaseStatus(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
Loading…
Reference in New Issue
Block a user