chore(web): migration svelte 5 syntax (#13883)

This commit is contained in:
Alex 2024-11-14 08:43:25 -06:00 committed by GitHub
parent 9203a61709
commit 0b3742cf13
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
310 changed files with 6435 additions and 4176 deletions

View file

@ -7,13 +7,17 @@
import { deleteUserAdmin, type UserResponseDto } from '@immich/sdk';
import { t } from 'svelte-i18n';
export let user: UserResponseDto;
export let onSuccess: () => void;
export let onFail: () => void;
export let onCancel: () => void;
interface Props {
user: UserResponseDto;
onSuccess: () => void;
onFail: () => void;
onCancel: () => void;
}
let forceDelete = false;
let deleteButtonDisabled = false;
let { user, onSuccess, onFail, onCancel }: Props = $props();
let forceDelete = $state(false);
let deleteButtonDisabled = $state(false);
let userIdInput: string = '';
const handleDeleteUser = async () => {
@ -47,12 +51,14 @@
{onCancel}
disabled={deleteButtonDisabled}
>
<svelte:fragment slot="prompt">
{#snippet promptSnippet()}
<div class="flex flex-col gap-4">
{#if forceDelete}
<p>
<FormatMessage key="admin.user_delete_immediately" values={{ user: user.name }} let:message>
<b>{message}</b>
<FormatMessage key="admin.user_delete_immediately" values={{ user: user.name }}>
{#snippet children({ message })}
<b>{message}</b>
{/snippet}
</FormatMessage>
</p>
{:else}
@ -60,9 +66,10 @@
<FormatMessage
key="admin.user_delete_delay"
values={{ user: user.name, delay: $serverConfig.userDeleteDelay }}
let:message
>
<b>{message}</b>
{#snippet children({ message })}
<b>{message}</b>
{/snippet}
</FormatMessage>
</p>
{/if}
@ -73,7 +80,7 @@
label={$t('admin.user_delete_immediately_checkbox')}
labelClass="text-sm dark:text-immich-dark-fg"
bind:checked={forceDelete}
on:change={() => {
onchange={() => {
deleteButtonDisabled = forceDelete;
}}
/>
@ -92,9 +99,9 @@
aria-describedby="confirm-user-desc"
name="confirm-user-id"
type="text"
on:input={handleConfirm}
oninput={handleConfirm}
/>
{/if}
</div>
</svelte:fragment>
{/snippet}
</ConfirmDialog>

View file

@ -1,10 +1,18 @@
<script lang="ts" context="module">
<script lang="ts" module>
export type Colors = 'light-gray' | 'gray' | 'dark-gray';
</script>
<script lang="ts">
export let color: Colors;
export let disabled = false;
import type { Snippet } from 'svelte';
interface Props {
color: Colors;
disabled?: boolean;
children?: Snippet;
onClick?: () => void;
}
let { color, disabled = false, onClick = () => {}, children }: Props = $props();
const colorClasses: Record<Colors, string> = {
'light-gray': 'bg-gray-300/80 dark:bg-gray-700',
@ -23,7 +31,7 @@
class="flex h-full w-full flex-col place-content-center place-items-center gap-2 px-8 py-2 text-xs text-gray-600 transition-colors dark:text-gray-200 {colorClasses[
color
]} {hoverClasses}"
on:click
onclick={onClick}
>
<slot />
{@render children?.()}
</button>

View file

@ -1,9 +1,16 @@
<script lang="ts" context="module">
<script lang="ts" module>
export type Color = 'success' | 'warning';
</script>
<script lang="ts">
export let color: Color;
import type { Snippet } from 'svelte';
interface Props {
color: Color;
children?: Snippet;
}
let { color, children }: Props = $props();
const colorClasses: Record<Color, string> = {
success: 'bg-green-500/70 text-gray-900 dark:bg-green-700/90 dark:text-gray-100',
@ -12,5 +19,5 @@
</script>
<div class="w-full p-2 text-center text-sm {colorClasses[color]}">
<slot />
{@render children?.()}
</div>

View file

@ -19,22 +19,37 @@
import JobTileButton from './job-tile-button.svelte';
import JobTileStatus from './job-tile-status.svelte';
export let title: string;
export let subtitle: string | undefined;
export let description: Component | undefined;
export let jobCounts: JobCountsDto;
export let queueStatus: QueueStatusDto;
export let icon: string;
export let disabled = false;
interface Props {
title: string;
subtitle: string | undefined;
description: Component | undefined;
jobCounts: JobCountsDto;
queueStatus: QueueStatusDto;
icon: string;
disabled?: boolean;
allText: string | undefined;
refreshText: string | undefined;
missingText: string;
onCommand: (command: JobCommandDto) => void;
}
export let allText: string | undefined;
export let refreshText: string | undefined;
export let missingText: string;
export let onCommand: (command: JobCommandDto) => void;
let {
title,
subtitle,
description,
jobCounts,
queueStatus,
icon,
disabled = false,
allText,
refreshText,
missingText,
onCommand,
}: Props = $props();
$: waitingCount = jobCounts.waiting + jobCounts.paused + jobCounts.delayed;
$: isIdle = !queueStatus.isActive && !queueStatus.isPaused;
$: multipleButtons = allText || refreshText;
let waitingCount = $derived(jobCounts.waiting + jobCounts.paused + jobCounts.delayed);
let isIdle = $derived(!queueStatus.isActive && !queueStatus.isPaused);
let multipleButtons = $derived(allText || refreshText);
const commonClasses = 'flex place-items-center justify-between w-full py-2 sm:py-4 pr-4 pl-6';
</script>
@ -67,7 +82,7 @@
title={$t('clear_message')}
size="12"
padding="1"
on:click={() => onCommand({ command: JobCommand.ClearFailed, force: false })}
onclick={() => onCommand({ command: JobCommand.ClearFailed, force: false })}
/>
</div>
</Badge>
@ -87,8 +102,9 @@
{/if}
{#if description}
{@const SvelteComponent = description}
<div class="text-sm dark:text-white">
<svelte:component this={description} />
<SvelteComponent />
</div>
{/if}
@ -118,7 +134,7 @@
<JobTileButton
disabled={true}
color="light-gray"
on:click={() => onCommand({ command: JobCommand.Start, force: false })}
onClick={() => onCommand({ command: JobCommand.Start, force: false })}
>
<Icon path={mdiAlertCircle} size="36" />
{$t('disabled').toUpperCase()}
@ -127,20 +143,20 @@
{#if !disabled && !isIdle}
{#if waitingCount > 0}
<JobTileButton color="gray" on:click={() => onCommand({ command: JobCommand.Empty, force: false })}>
<JobTileButton color="gray" onClick={() => onCommand({ command: JobCommand.Empty, force: false })}>
<Icon path={mdiClose} size="24" />
{$t('clear').toUpperCase()}
</JobTileButton>
{/if}
{#if queueStatus.isPaused}
{@const size = waitingCount > 0 ? '24' : '48'}
<JobTileButton color="light-gray" on:click={() => onCommand({ command: JobCommand.Resume, force: false })}>
<JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Resume, force: false })}>
<!-- size property is not reactive, so have to use width and height -->
<Icon path={mdiFastForward} {size} />
{$t('resume').toUpperCase()}
</JobTileButton>
{:else}
<JobTileButton color="light-gray" on:click={() => onCommand({ command: JobCommand.Pause, force: false })}>
<JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Pause, force: false })}>
<Icon path={mdiPause} size="24" />
{$t('pause').toUpperCase()}
</JobTileButton>
@ -149,25 +165,25 @@
{#if !disabled && multipleButtons && isIdle}
{#if allText}
<JobTileButton color="dark-gray" on:click={() => onCommand({ command: JobCommand.Start, force: true })}>
<JobTileButton color="dark-gray" onClick={() => onCommand({ command: JobCommand.Start, force: true })}>
<Icon path={mdiAllInclusive} size="24" />
{allText}
</JobTileButton>
{/if}
{#if refreshText}
<JobTileButton color="gray" on:click={() => onCommand({ command: JobCommand.Start, force: undefined })}>
<JobTileButton color="gray" onClick={() => onCommand({ command: JobCommand.Start, force: undefined })}>
<Icon path={mdiImageRefreshOutline} size="24" />
{refreshText}
</JobTileButton>
{/if}
<JobTileButton color="light-gray" on:click={() => onCommand({ command: JobCommand.Start, force: false })}>
<JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Start, force: false })}>
<Icon path={mdiSelectionSearch} size="24" />
{missingText}
</JobTileButton>
{/if}
{#if !disabled && !multipleButtons && isIdle}
<JobTileButton color="light-gray" on:click={() => onCommand({ command: JobCommand.Start, force: false })}>
<JobTileButton color="light-gray" onClick={() => onCommand({ command: JobCommand.Start, force: false })}>
<Icon path={mdiPlay} size="48" />
{$t('start').toUpperCase()}
</JobTileButton>

View file

@ -25,7 +25,11 @@
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
import { t } from 'svelte-i18n';
export let jobs: AllJobStatusResponseDto;
interface Props {
jobs: AllJobStatusResponseDto;
}
let { jobs = $bindable() }: Props = $props();
interface JobDetails {
title: string;
@ -56,8 +60,7 @@
await handleCommand(jobId, dto);
};
// svelte-ignore reactive_declaration_non_reactive_property
$: jobDetails = <Partial<Record<JobName, JobDetails>>>{
let jobDetails: Partial<Record<JobName, JobDetails>> = {
[JobName.ThumbnailGeneration]: {
icon: mdiFileJpgBox,
title: $getJobName(JobName.ThumbnailGeneration),
@ -142,7 +145,8 @@
missingText: $t('missing'),
},
};
$: jobList = Object.entries(jobDetails) as [JobName, JobDetails][];
let jobList = Object.entries(jobDetails) as [JobName, JobDetails][];
async function handleCommand(jobId: JobName, jobCommand: JobCommandDto) {
const title = jobDetails[jobId]?.title;

View file

@ -7,12 +7,13 @@
<FormatMessage
key="admin.storage_template_migration_description"
values={{ template: $t('admin.storage_template_settings') }}
let:message
>
<a
href="{AppRoute.ADMIN_SETTINGS}?{QueryParameter.IS_OPEN}={OpenSettingQueryParameterValue.STORAGE_TEMPLATE}"
class="text-immich-primary dark:text-immich-dark-primary"
>
{message}
</a>
{#snippet children({ message })}
<a
href="{AppRoute.ADMIN_SETTINGS}?{QueryParameter.IS_OPEN}={OpenSettingQueryParameterValue.STORAGE_TEMPLATE}"
class="text-immich-primary dark:text-immich-dark-primary"
>
{message}
</a>
{/snippet}
</FormatMessage>

View file

@ -5,10 +5,14 @@
import { restoreUserAdmin, type UserResponseDto } from '@immich/sdk';
import { t } from 'svelte-i18n';
export let user: UserResponseDto;
export let onSuccess: () => void;
export let onFail: () => void;
export let onCancel: () => void;
interface Props {
user: UserResponseDto;
onSuccess: () => void;
onFail: () => void;
onCancel: () => void;
}
let { user, onSuccess, onFail, onCancel }: Props = $props();
const handleRestoreUser = async () => {
try {
@ -32,11 +36,13 @@
onConfirm={handleRestoreUser}
{onCancel}
>
<svelte:fragment slot="prompt">
{#snippet promptSnippet()}
<p>
<FormatMessage key="admin.user_restore_description" values={{ user: user.name }} let:message>
<b>{message}</b>
<FormatMessage key="admin.user_restore_description" values={{ user: user.name }}>
{#snippet children({ message })}
<b>{message}</b>
{/snippet}
</FormatMessage>
</p>
</svelte:fragment>
{/snippet}
</ConfirmDialog>

View file

@ -7,14 +7,20 @@
import StatsCard from './stats-card.svelte';
import { t } from 'svelte-i18n';
export let stats: ServerStatsResponseDto = {
photos: 0,
videos: 0,
usage: 0,
usageByUser: [],
};
interface Props {
stats?: ServerStatsResponseDto;
}
$: zeros = (value: number) => {
let {
stats = {
photos: 0,
videos: 0,
usage: 0,
usageByUser: [],
},
}: Props = $props();
const zeros = (value: number) => {
const maxLength = 13;
const valueLength = value.toString().length;
const zeroLength = maxLength - valueLength;
@ -23,7 +29,7 @@
};
const TiB = 1024 ** 4;
$: [statsUsage, statsUsageUnit] = getBytesWithUnit(stats.usage, stats.usage > TiB ? 2 : 0);
let [statsUsage, statsUsageUnit] = $derived(getBytesWithUnit(stats.usage, stats.usage > TiB ? 2 : 0));
</script>
<div class="flex flex-col gap-5">

View file

@ -2,18 +2,22 @@
import Icon from '$lib/components/elements/icon.svelte';
import { ByteUnit } from '$lib/utils/byte-units';
export let icon: string;
export let title: string;
export let value: number;
export let unit: ByteUnit | undefined = undefined;
interface Props {
icon: string;
title: string;
value: number;
unit?: ByteUnit | undefined;
}
$: zeros = () => {
let { icon, title, value, unit = undefined }: Props = $props();
const zeros = $derived(() => {
const maxLength = 13;
const valueLength = value.toString().length;
const zeroLength = maxLength - valueLength;
return '0'.repeat(zeroLength);
};
});
</script>
<div class="flex h-[140px] w-[250px] flex-col justify-between rounded-3xl bg-immich-gray p-5 dark:bg-immich-dark-gray">

View file

@ -1,5 +1,3 @@
<svelte:options accessors />
<script lang="ts">
import {
NotificationType,
@ -13,12 +11,17 @@
import type { SettingsResetOptions } from './admin-settings';
import { t } from 'svelte-i18n';
export let config: SystemConfigDto;
interface Props {
config: SystemConfigDto;
children: import('svelte').Snippet<[{ savedConfig: SystemConfigDto; defaultConfig: SystemConfigDto }]>;
}
let savedConfig: SystemConfigDto;
let defaultConfig: SystemConfigDto;
let { config = $bindable(), children }: Props = $props();
const handleReset = async (options: SettingsResetOptions) => {
let savedConfig: SystemConfigDto | undefined = $state();
let defaultConfig: SystemConfigDto | undefined = $state();
export const handleReset = async (options: SettingsResetOptions) => {
await (options.default ? resetToDefault(options.configKeys) : reset(options.configKeys));
};
@ -26,7 +29,8 @@
let systemConfigDto = {
...savedConfig,
...update,
};
} as SystemConfigDto;
if (isEqual(systemConfigDto, savedConfig)) {
return;
}
@ -59,6 +63,10 @@
};
const resetToDefault = (configKeys: Array<keyof SystemConfigDto>) => {
if (!defaultConfig) {
return;
}
for (const key of configKeys) {
config = { ...config, [key]: defaultConfig[key] };
}
@ -75,5 +83,5 @@
</script>
{#if savedConfig && defaultConfig}
<slot {handleReset} {handleSave} {savedConfig} {defaultConfig} />
{@render children({ savedConfig, defaultConfig })}
{/if}

View file

@ -2,9 +2,7 @@
import ConfirmDialog from '$lib/components/shared-components/dialog/confirm-dialog.svelte';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { type SystemConfigDto } from '@immich/sdk';
import { isEqual } from 'lodash-es';
@ -12,15 +10,20 @@
import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte';
import { SettingInputFieldType } from '$lib/constants';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false;
export let onReset: SettingsResetEvent;
export let onSave: SettingsSaveEvent;
interface Props {
savedConfig: SystemConfigDto;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
let isConfirmOpen = false;
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
let isConfirmOpen = $state(false);
const handleToggleOverride = () => {
// click runs before bind
@ -48,29 +51,31 @@
onCancel={() => (isConfirmOpen = false)}
onConfirm={() => handleSave(true)}
>
<svelte:fragment slot="prompt">
{#snippet promptSnippet()}
<div class="flex flex-col gap-4">
<p>{$t('admin.authentication_settings_disable_all')}</p>
<p>
<FormatMessage key="admin.authentication_settings_reenable" let:message>
<a
href="https://immich.app/docs/administration/server-commands"
rel="noreferrer"
target="_blank"
class="underline"
>
{message}
</a>
<FormatMessage key="admin.authentication_settings_reenable">
{#snippet children({ message })}
<a
href="https://immich.app/docs/administration/server-commands"
rel="noreferrer"
target="_blank"
class="underline"
>
{message}
</a>
{/snippet}
</FormatMessage>
</p>
</div>
</svelte:fragment>
{/snippet}
</ConfirmDialog>
{/if}
<div>
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<form autocomplete="off" onsubmit={(e) => e.preventDefault()}>
<div class="ml-4 mt-4 flex flex-col">
<SettingAccordion
key="oauth"
@ -79,15 +84,17 @@
>
<div class="ml-4 mt-4 flex flex-col gap-4">
<p class="text-sm dark:text-immich-dark-fg">
<FormatMessage key="admin.oauth_settings_more_details" let:message>
<a
href="https://immich.app/docs/administration/oauth"
class="underline"
target="_blank"
rel="noreferrer"
>
{message}
</a>
<FormatMessage key="admin.oauth_settings_more_details">
{#snippet children({ message })}
<a
href="https://immich.app/docs/administration/oauth"
class="underline"
target="_blank"
rel="noreferrer"
>
{message}
</a>
{/snippet}
</FormatMessage>
</p>
@ -147,7 +154,7 @@
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label={$t('admin.oauth_profile_signing_algorithm').toUpperCase()}
desc={$t('admin.oauth_profile_signing_algorithm_description')}
description={$t('admin.oauth_profile_signing_algorithm_description')}
bind:value={config.oauth.profileSigningAlgorithm}
required={true}
disabled={disabled || !config.oauth.enabled}
@ -157,7 +164,7 @@
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label={$t('admin.oauth_storage_label_claim').toUpperCase()}
desc={$t('admin.oauth_storage_label_claim_description')}
description={$t('admin.oauth_storage_label_claim_description')}
bind:value={config.oauth.storageLabelClaim}
required={true}
disabled={disabled || !config.oauth.enabled}
@ -167,7 +174,7 @@
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label={$t('admin.oauth_storage_quota_claim').toUpperCase()}
desc={$t('admin.oauth_storage_quota_claim_description')}
description={$t('admin.oauth_storage_quota_claim_description')}
bind:value={config.oauth.storageQuotaClaim}
required={true}
disabled={disabled || !config.oauth.enabled}
@ -177,7 +184,7 @@
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.oauth_storage_quota_default').toUpperCase()}
desc={$t('admin.oauth_storage_quota_default_description')}
description={$t('admin.oauth_storage_quota_default_description')}
bind:value={config.oauth.defaultStorageQuota}
required={true}
disabled={disabled || !config.oauth.enabled}
@ -213,7 +220,7 @@
values: { callback: 'app.immich:///oauth-callback' },
})}
disabled={disabled || !config.oauth.enabled}
on:click={() => handleToggleOverride()}
onToggle={() => handleToggleOverride()}
bind:checked={config.oauth.mobileOverrideEnabled}
/>

View file

@ -3,33 +3,40 @@
import { isEqual } from 'lodash-es';
import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte';
import { SettingInputFieldType } from '$lib/constants';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false;
export let onReset: SettingsResetEvent;
export let onSave: SettingsSaveEvent;
interface Props {
savedConfig: SystemConfigDto;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
$: cronExpressionOptions = [
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
let cronExpressionOptions = $derived([
{ text: $t('interval.night_at_midnight'), value: '0 0 * * *' },
{ text: $t('interval.night_at_twoam'), value: '0 02 * * *' },
{ text: $t('interval.day_at_onepm'), value: '0 13 * * *' },
{ text: $t('interval.hours', { values: { hours: 6 } }), value: '0 */6 * * *' },
];
]);
const onsubmit = (event: Event) => {
event.preventDefault();
};
</script>
<div>
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<form autocomplete="off" {onsubmit}>
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingSwitch
title={$t('admin.backup_database_enable_description')}
@ -53,21 +60,23 @@
bind:value={config.backup.database.cronExpression}
isEdited={config.backup.database.cronExpression !== savedConfig.backup.database.cronExpression}
>
<svelte:fragment slot="desc">
{#snippet descriptionSnippet()}
<p class="text-sm dark:text-immich-dark-fg">
<FormatMessage key="admin.cron_expression_description" let:message>
<a
href="https://crontab.guru/#{config.backup.database.cronExpression.replaceAll(' ', '_')}"
class="underline"
target="_blank"
rel="noreferrer"
>
{message}
<br />
</a>
<FormatMessage key="admin.cron_expression_description">
{#snippet children({ message })}
<a
href="https://crontab.guru/#{config.backup.database.cronExpression.replaceAll(' ', '_')}"
class="underline"
target="_blank"
rel="noreferrer"
>
{message}
<br />
</a>
{/snippet}
</FormatMessage>
</p>
</svelte:fragment>
{/snippet}
</SettingInputField>
<SettingInputField

View file

@ -15,44 +15,53 @@
import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingCheckboxes from '$lib/components/shared-components/settings/setting-checkboxes.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte';
import { SettingInputFieldType } from '$lib/constants';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false;
export let onReset: SettingsResetEvent;
export let onSave: SettingsSaveEvent;
interface Props {
savedConfig: SystemConfigDto;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
const onsubmit = (event: Event) => {
event.preventDefault();
};
</script>
<div>
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<form autocomplete="off" {onsubmit}>
<div class="ml-4 mt-4 flex flex-col gap-4">
<p class="text-sm dark:text-immich-dark-fg">
<Icon path={mdiHelpCircleOutline} class="inline" size="15" />
<FormatMessage key="admin.transcoding_codecs_learn_more" let:tag let:message>
{#if tag === 'h264-link'}
<a href="https://trac.ffmpeg.org/wiki/Encode/H.264" class="underline" target="_blank" rel="noreferrer">
{message}
</a>
{:else if tag === 'hevc-link'}
<a href="https://trac.ffmpeg.org/wiki/Encode/H.265" class="underline" target="_blank" rel="noreferrer">
{message}
</a>
{:else if tag === 'vp9-link'}
<a href="https://trac.ffmpeg.org/wiki/Encode/VP9" class="underline" target="_blank" rel="noreferrer">
{message}
</a>
{/if}
<FormatMessage key="admin.transcoding_codecs_learn_more">
{#snippet children({ tag, message })}
{#if tag === 'h264-link'}
<a href="https://trac.ffmpeg.org/wiki/Encode/H.264" class="underline" target="_blank" rel="noreferrer">
{message}
</a>
{:else if tag === 'hevc-link'}
<a href="https://trac.ffmpeg.org/wiki/Encode/H.265" class="underline" target="_blank" rel="noreferrer">
{message}
</a>
{:else if tag === 'vp9-link'}
<a href="https://trac.ffmpeg.org/wiki/Encode/VP9" class="underline" target="_blank" rel="noreferrer">
{message}
</a>
{/if}
{/snippet}
</FormatMessage>
</p>
@ -60,7 +69,7 @@
inputType={SettingInputFieldType.NUMBER}
{disabled}
label={$t('admin.transcoding_constant_rate_factor')}
desc={$t('admin.transcoding_constant_rate_factor_description')}
description={$t('admin.transcoding_constant_rate_factor_description')}
bind:value={config.ffmpeg.crf}
required={true}
isEdited={config.ffmpeg.crf !== savedConfig.ffmpeg.crf}
@ -186,7 +195,7 @@
inputType={SettingInputFieldType.TEXT}
{disabled}
label={$t('admin.transcoding_max_bitrate')}
desc={$t('admin.transcoding_max_bitrate_description')}
description={$t('admin.transcoding_max_bitrate_description')}
bind:value={config.ffmpeg.maxBitrate}
isEdited={config.ffmpeg.maxBitrate !== savedConfig.ffmpeg.maxBitrate}
/>
@ -195,7 +204,7 @@
inputType={SettingInputFieldType.NUMBER}
{disabled}
label={$t('admin.transcoding_threads')}
desc={$t('admin.transcoding_threads_description')}
description={$t('admin.transcoding_threads_description')}
bind:value={config.ffmpeg.threads}
isEdited={config.ffmpeg.threads !== savedConfig.ffmpeg.threads}
/>
@ -329,7 +338,7 @@
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label={$t('admin.transcoding_preferred_hardware_device')}
desc={$t('admin.transcoding_preferred_hardware_device_description')}
description={$t('admin.transcoding_preferred_hardware_device_description')}
bind:value={config.ffmpeg.preferredHwDevice}
isEdited={config.ffmpeg.preferredHwDevice !== savedConfig.ffmpeg.preferredHwDevice}
{disabled}
@ -346,7 +355,7 @@
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.transcoding_max_b_frames')}
desc={$t('admin.transcoding_max_b_frames_description')}
description={$t('admin.transcoding_max_b_frames_description')}
bind:value={config.ffmpeg.bframes}
isEdited={config.ffmpeg.bframes !== savedConfig.ffmpeg.bframes}
{disabled}
@ -355,7 +364,7 @@
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.transcoding_reference_frames')}
desc={$t('admin.transcoding_reference_frames_description')}
description={$t('admin.transcoding_reference_frames_description')}
bind:value={config.ffmpeg.refs}
isEdited={config.ffmpeg.refs !== savedConfig.ffmpeg.refs}
{disabled}
@ -364,7 +373,7 @@
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.transcoding_max_keyframe_interval')}
desc={$t('admin.transcoding_max_keyframe_interval_description')}
description={$t('admin.transcoding_max_keyframe_interval_description')}
bind:value={config.ffmpeg.gopSize}
isEdited={config.ffmpeg.gopSize !== savedConfig.ffmpeg.gopSize}
{disabled}

View file

@ -7,24 +7,39 @@
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import { t } from 'svelte-i18n';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
import { SettingInputFieldType } from '$lib/constants';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false;
export let onReset: SettingsResetEvent;
export let onSave: SettingsSaveEvent;
export let openByDefault = false;
interface Props {
savedConfig: SystemConfigDto;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
openByDefault?: boolean;
}
let {
savedConfig,
defaultConfig,
config = $bindable(),
disabled = false,
onReset,
onSave,
openByDefault = false,
}: Props = $props();
const onsubmit = (event: Event) => {
event.preventDefault();
};
</script>
<div>
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<form autocomplete="off" {onsubmit}>
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingAccordion
key="thumbnail-settings"
@ -65,7 +80,7 @@
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.image_quality')}
desc={$t('admin.image_thumbnail_quality_description')}
description={$t('admin.image_thumbnail_quality_description')}
bind:value={config.image.thumbnail.quality}
isEdited={config.image.thumbnail.quality !== savedConfig.image.thumbnail.quality}
{disabled}
@ -110,7 +125,7 @@
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.image_quality')}
desc={$t('admin.image_preview_quality_description')}
description={$t('admin.image_preview_quality_description')}
bind:value={config.image.preview.quality}
isEdited={config.image.preview.quality !== savedConfig.image.preview.quality}
{disabled}

View file

@ -5,17 +5,20 @@
import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import { t } from 'svelte-i18n';
import { SettingInputFieldType } from '$lib/constants';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false;
export let onReset: SettingsResetEvent;
export let onSave: SettingsSaveEvent;
interface Props {
savedConfig: SystemConfigDto;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
const jobNames = [
JobName.ThumbnailGeneration,
@ -34,11 +37,15 @@
function isSystemConfigJobDto(jobName: any): jobName is keyof SystemConfigJobDto {
return jobName in config.job;
}
const onsubmit = (event: Event) => {
event.preventDefault();
};
</script>
<div>
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<form autocomplete="off" {onsubmit}>
{#each jobNames as jobName}
<div class="ml-4 mt-4 flex flex-col gap-4">
{#if isSystemConfigJobDto(jobName)}
@ -46,7 +53,7 @@
inputType={SettingInputFieldType.NUMBER}
{disabled}
label={$t('admin.job_concurrency', { values: { job: $getJobName(jobName) } })}
desc=""
description=""
bind:value={config.job[jobName].concurrency}
required={true}
isEdited={!(config.job[jobName].concurrency == savedConfig.job[jobName].concurrency)}
@ -55,7 +62,7 @@
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.job_concurrency', { values: { job: $getJobName(jobName) } })}
desc=""
description=""
value="1"
disabled={true}
title={$t('admin.job_not_concurrency_safe')}

View file

@ -4,34 +4,49 @@
import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte';
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import { SettingInputFieldType } from '$lib/constants';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false;
export let onReset: SettingsResetEvent;
export let onSave: SettingsSaveEvent;
export let openByDefault = false;
interface Props {
savedConfig: SystemConfigDto;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
openByDefault?: boolean;
}
$: cronExpressionOptions = [
let {
savedConfig,
defaultConfig,
config = $bindable(),
disabled = false,
onReset,
onSave,
openByDefault = false,
}: Props = $props();
let cronExpressionOptions = $derived([
{ text: $t('interval.night_at_midnight'), value: '0 0 * * *' },
{ text: $t('interval.night_at_twoam'), value: '0 2 * * *' },
{ text: $t('interval.day_at_onepm'), value: '0 13 * * *' },
{ text: $t('interval.hours', { values: { hours: 6 } }), value: '0 */6 * * *' },
];
]);
const onsubmit = (event: Event) => {
event.preventDefault();
};
</script>
<div>
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<form autocomplete="off" {onsubmit}>
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingAccordion
key="library-watching"
@ -77,20 +92,22 @@
bind:value={config.library.scan.cronExpression}
isEdited={config.library.scan.cronExpression !== savedConfig.library.scan.cronExpression}
>
<svelte:fragment slot="desc">
{#snippet descriptionSnippet()}
<p class="text-sm dark:text-immich-dark-fg">
<FormatMessage key="admin.cron_expression_description" let:message>
<a
href="https://crontab.guru/#{config.library.scan.cronExpression.replaceAll(' ', '_')}"
class="underline"
target="_blank"
rel="noreferrer"
>
{message}
</a>
<FormatMessage key="admin.cron_expression_description">
{#snippet children({ message })}
<a
href="https://crontab.guru/#{config.library.scan.cronExpression.replaceAll(' ', '_')}"
class="underline"
target="_blank"
rel="noreferrer"
>
{message}
</a>
{/snippet}
</FormatMessage>
</p>
</svelte:fragment>
{/snippet}
</SettingInputField>
</div>
</SettingAccordion>

View file

@ -8,17 +8,25 @@
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import { t } from 'svelte-i18n';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false;
export let onReset: SettingsResetEvent;
export let onSave: SettingsSaveEvent;
interface Props {
savedConfig: SystemConfigDto;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
const onsubmit = (event: Event) => {
event.preventDefault();
};
</script>
<div>
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<form autocomplete="off" {onsubmit}>
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingSwitch
title={$t('admin.logging_enable_description')}

View file

@ -5,26 +5,33 @@
import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { featureFlags } from '$lib/stores/server-config.store';
import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte';
import { SettingInputFieldType } from '$lib/constants';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false;
export let onReset: SettingsResetEvent;
export let onSave: SettingsSaveEvent;
interface Props {
savedConfig: SystemConfigDto;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
const onsubmit = (event: Event) => {
event.preventDefault();
};
</script>
<div class="mt-2">
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault class="mx-4 mt-4">
<form autocomplete="off" {onsubmit} class="mx-4 mt-4">
<div class="flex flex-col gap-4">
<SettingSwitch
title={$t('admin.machine_learning_enabled')}
@ -38,7 +45,7 @@
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label={$t('url')}
desc={$t('admin.machine_learning_url_description')}
description={$t('admin.machine_learning_url_description')}
bind:value={config.machineLearning.url}
required={true}
disabled={disabled || !config.machineLearning.enabled}
@ -69,11 +76,15 @@
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.clip.enabled}
isEdited={config.machineLearning.clip.modelName !== savedConfig.machineLearning.clip.modelName}
>
<p slot="desc" class="immich-form-label pb-2 text-sm">
<FormatMessage key="admin.machine_learning_clip_model_description" let:message>
<a href="https://huggingface.co/immich-app"><u>{message}</u></a>
</FormatMessage>
</p>
{#snippet descriptionSnippet()}
<p class="immich-form-label pb-2 text-sm">
<FormatMessage key="admin.machine_learning_clip_model_description">
{#snippet children({ message })}
<a href="https://huggingface.co/immich-app"><u>{message}</u></a>
{/snippet}
</FormatMessage>
</p>
{/snippet}
</SettingInputField>
</div>
</SettingAccordion>
@ -100,7 +111,7 @@
step="0.0005"
min={0.001}
max={0.1}
desc={$t('admin.machine_learning_max_detection_distance_description')}
description={$t('admin.machine_learning_max_detection_distance_description')}
disabled={disabled || !$featureFlags.duplicateDetection}
isEdited={config.machineLearning.duplicateDetection.maxDistance !==
savedConfig.machineLearning.duplicateDetection.maxDistance}
@ -142,7 +153,7 @@
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_min_detection_score')}
desc={$t('admin.machine_learning_min_detection_score_description')}
description={$t('admin.machine_learning_min_detection_score_description')}
bind:value={config.machineLearning.facialRecognition.minScore}
step="0.1"
min={0.1}
@ -155,7 +166,7 @@
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_max_recognition_distance')}
desc={$t('admin.machine_learning_max_recognition_distance_description')}
description={$t('admin.machine_learning_max_recognition_distance_description')}
bind:value={config.machineLearning.facialRecognition.maxDistance}
step="0.1"
min={0.1}
@ -168,7 +179,7 @@
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_min_recognized_faces')}
desc={$t('admin.machine_learning_min_recognized_faces_description')}
description={$t('admin.machine_learning_min_recognized_faces_description')}
bind:value={config.machineLearning.facialRecognition.minFaces}
step="1"
min={1}

View file

@ -6,23 +6,30 @@
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte';
import { SettingInputFieldType } from '$lib/constants';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false;
export let onReset: SettingsResetEvent;
export let onSave: SettingsSaveEvent;
interface Props {
savedConfig: SystemConfigDto;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
const onsubmit = (event: Event) => {
event.preventDefault();
};
</script>
<div class="mt-2">
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<form autocomplete="off" {onsubmit}>
<div class="flex flex-col gap-4">
<SettingAccordion key="map" title={$t('admin.map_settings')} subtitle={$t('admin.map_settings_description')}>
<div class="ml-4 mt-4 flex flex-col gap-4">
@ -38,7 +45,7 @@
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label={$t('admin.map_light_style')}
desc={$t('admin.map_style_description')}
description={$t('admin.map_style_description')}
bind:value={config.map.lightStyle}
disabled={disabled || !config.map.enabled}
isEdited={config.map.lightStyle !== savedConfig.map.lightStyle}
@ -46,7 +53,7 @@
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label={$t('admin.map_dark_style')}
desc={$t('admin.map_style_description')}
description={$t('admin.map_style_description')}
bind:value={config.map.darkStyle}
disabled={disabled || !config.map.enabled}
isEdited={config.map.darkStyle !== savedConfig.map.darkStyle}
@ -55,20 +62,22 @@
>
<SettingAccordion key="reverse-geocoding" title={$t('admin.map_reverse_geocoding_settings')}>
<svelte:fragment slot="subtitle">
{#snippet subtitleSnippet()}
<p class="text-sm dark:text-immich-dark-fg">
<FormatMessage key="admin.map_manage_reverse_geocoding_settings" let:message>
<a
href="https://immich.app/docs/features/reverse-geocoding"
class="underline"
target="_blank"
rel="noreferrer"
>
{message}
</a>
<FormatMessage key="admin.map_manage_reverse_geocoding_settings">
{#snippet children({ message })}
<a
href="https://immich.app/docs/features/reverse-geocoding"
class="underline"
target="_blank"
rel="noreferrer"
>
{message}
</a>
{/snippet}
</FormatMessage>
</p>
</svelte:fragment>
{/snippet}
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingSwitch
title={$t('admin.map_reverse_geocoding_enable_description')}

View file

@ -7,17 +7,25 @@
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { t } from 'svelte-i18n';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false;
export let onReset: SettingsResetEvent;
export let onSave: SettingsSaveEvent;
interface Props {
savedConfig: SystemConfigDto;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
const onsubmit = (event: Event) => {
event.preventDefault();
};
</script>
<div class="mt-2">
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault class="mx-4 mt-4">
<form autocomplete="off" {onsubmit} class="mx-4 mt-4">
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingSwitch
title={$t('admin.metadata_faces_import_setting')}

View file

@ -7,17 +7,25 @@
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { t } from 'svelte-i18n';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false;
export let onReset: SettingsResetEvent;
export let onSave: SettingsSaveEvent;
interface Props {
savedConfig: SystemConfigDto;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
const onsubmit = (event: Event) => {
event.preventDefault();
};
</script>
<div>
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<form autocomplete="off" {onsubmit}>
<div class="ml-4 mt-4">
<SettingSwitch
title={$t('admin.version_check_enabled_description')}

View file

@ -3,9 +3,7 @@
import { isEqual } from 'lodash-es';
import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
@ -18,15 +16,20 @@
import { user } from '$lib/stores/user.store';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { handleError } from '$lib/utils/handle-error';
import { SettingInputFieldType } from '$lib/constants';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false;
export let onReset: SettingsResetEvent;
export let onSave: SettingsSaveEvent;
interface Props {
savedConfig: SystemConfigDto;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
let isSending = false;
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
let isSending = $state(false);
const handleSendTestEmail = async () => {
if (isSending) {
@ -65,11 +68,15 @@
isSending = false;
}
};
const onsubmit = (event: Event) => {
event.preventDefault();
};
</script>
<div>
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault class="mt-4">
<form autocomplete="off" {onsubmit} class="mt-4">
<div class="flex flex-col gap-4">
<SettingAccordion key="email" title={$t('email')} subtitle={$t('admin.notification_email_setting_description')}>
<div class="ml-4 mt-4 flex flex-col gap-4">
@ -85,7 +92,7 @@
inputType={SettingInputFieldType.TEXT}
required
label={$t('host')}
desc={$t('admin.notification_email_host_description')}
description={$t('admin.notification_email_host_description')}
disabled={disabled || !config.notifications.smtp.enabled}
bind:value={config.notifications.smtp.transport.host}
isEdited={config.notifications.smtp.transport.host !== savedConfig.notifications.smtp.transport.host}
@ -95,7 +102,7 @@
inputType={SettingInputFieldType.NUMBER}
required
label={$t('port')}
desc={$t('admin.notification_email_port_description')}
description={$t('admin.notification_email_port_description')}
disabled={disabled || !config.notifications.smtp.enabled}
bind:value={config.notifications.smtp.transport.port}
isEdited={config.notifications.smtp.transport.port !== savedConfig.notifications.smtp.transport.port}
@ -104,7 +111,7 @@
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label={$t('username')}
desc={$t('admin.notification_email_username_description')}
description={$t('admin.notification_email_username_description')}
disabled={disabled || !config.notifications.smtp.enabled}
bind:value={config.notifications.smtp.transport.username}
isEdited={config.notifications.smtp.transport.username !==
@ -114,7 +121,7 @@
<SettingInputField
inputType={SettingInputFieldType.PASSWORD}
label={$t('password')}
desc={$t('admin.notification_email_password_description')}
description={$t('admin.notification_email_password_description')}
disabled={disabled || !config.notifications.smtp.enabled}
bind:value={config.notifications.smtp.transport.password}
isEdited={config.notifications.smtp.transport.password !==
@ -134,14 +141,14 @@
inputType={SettingInputFieldType.TEXT}
required
label={$t('admin.notification_email_from_address')}
desc={$t('admin.notification_email_from_address_description')}
description={$t('admin.notification_email_from_address_description')}
disabled={disabled || !config.notifications.smtp.enabled}
bind:value={config.notifications.smtp.from}
isEdited={config.notifications.smtp.from !== savedConfig.notifications.smtp.from}
/>
<div class="flex gap-2 place-items-center">
<Button size="sm" disabled={!config.notifications.smtp.enabled} on:click={handleSendTestEmail}>
<Button size="sm" disabled={!config.notifications.smtp.enabled} onclick={handleSendTestEmail}>
{#if disabled}
{$t('admin.notification_email_test_email')}
{:else}

View file

@ -3,28 +3,35 @@
import { isEqual } from 'lodash-es';
import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import { t } from 'svelte-i18n';
import { SettingInputFieldType } from '$lib/constants';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false;
export let onReset: SettingsResetEvent;
export let onSave: SettingsSaveEvent;
interface Props {
savedConfig: SystemConfigDto;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
const onsubmit = (event: Event) => {
event.preventDefault();
};
</script>
<div>
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<form autocomplete="off" {onsubmit}>
<div class="mt-4 ml-4">
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label={$t('admin.server_external_domain_settings')}
desc={$t('admin.server_external_domain_settings_description')}
description={$t('admin.server_external_domain_settings_description')}
bind:value={config.server.externalDomain}
isEdited={config.server.externalDomain !== savedConfig.server.externalDomain}
/>
@ -32,7 +39,7 @@
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label={$t('admin.server_welcome_message')}
desc={$t('admin.server_welcome_message_description')}
description={$t('admin.server_welcome_message_description')}
bind:value={config.server.loginPageMessage}
isEdited={config.server.loginPageMessage !== savedConfig.server.loginPageMessage}
/>

View file

@ -1,6 +1,9 @@
<script lang="ts">
import { createBubbler, preventDefault } from 'svelte/legacy';
const bubble = createBubbler();
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { AppRoute } from '$lib/constants';
import { AppRoute, SettingInputFieldType } from '$lib/constants';
import { user } from '$lib/stores/user.store';
import {
getStorageTemplateOptions,
@ -15,24 +18,38 @@
import SupportedDatetimePanel from './supported-datetime-panel.svelte';
import SupportedVariablesPanel from './supported-variables-panel.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { t } from 'svelte-i18n';
import FormatMessage from '$lib/components/i18n/format-message.svelte';
import type { Snippet } from 'svelte';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false;
export let minified = false;
export let onReset: SettingsResetEvent;
export let onSave: SettingsSaveEvent;
export let duration: number = 500;
interface Props {
savedConfig: SystemConfigDto;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
disabled?: boolean;
minified?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
duration?: number;
children?: Snippet;
}
let templateOptions: SystemConfigTemplateStorageOptionDto;
let selectedPreset = '';
let {
savedConfig,
defaultConfig,
config = $bindable(),
disabled = false,
minified = false,
onReset,
onSave,
duration = 500,
children,
}: Props = $props();
let templateOptions: SystemConfigTemplateStorageOptionDto | undefined = $state();
let selectedPreset = $state('');
const getTemplateOptions = async () => {
templateOptions = await getStorageTemplateOptions();
@ -41,15 +58,11 @@
const getSupportDateTimeFormat = () => getStorageTemplateOptions();
$: parsedTemplate = () => {
try {
return renderTemplate(config.storageTemplate.template);
} catch {
return 'error';
}
};
const renderTemplate = (templateString: string) => {
if (!templateOptions) {
return '';
}
const template = handlebar.compile(templateString, {
knownHelpers: undefined,
});
@ -85,31 +98,40 @@
const handlePresetSelection = () => {
config.storageTemplate.template = selectedPreset;
};
let parsedTemplate = $derived(() => {
try {
return renderTemplate(config.storageTemplate.template);
} catch {
return 'error';
}
});
</script>
<section class="dark:text-immich-dark-fg mt-2">
<div in:fade={{ duration }} class="mx-4 flex flex-col gap-4 py-4">
<p class="text-sm dark:text-immich-dark-fg">
<FormatMessage key="admin.storage_template_more_details" let:tag let:message>
{#if tag === 'template-link'}
<a
href="https://immich.app/docs/administration/storage-template"
class="underline"
target="_blank"
rel="noreferrer"
>
{message}
</a>
{:else if tag === 'implications-link'}
<a
href="https://immich.app/docs/administration/backup-and-restore#asset-types-and-storage-locations"
class="underline"
target="_blank"
rel="noreferrer"
>
{message}
</a>
{/if}
<FormatMessage key="admin.storage_template_more_details">
{#snippet children({ tag, message })}
{#if tag === 'template-link'}
<a
href="https://immich.app/docs/administration/storage-template"
class="underline"
target="_blank"
rel="noreferrer"
>
{message}
</a>
{:else if tag === 'implications-link'}
<a
href="https://immich.app/docs/administration/backup-and-restore#asset-types-and-storage-locations"
class="underline"
target="_blank"
rel="noreferrer"
>
{message}
</a>
{/if}
{/snippet}
</FormatMessage>
</p>
</div>
@ -164,19 +186,18 @@
<FormatMessage
key="admin.storage_template_path_length"
values={{ length: parsedTemplate().length + $user.id.length + 'UPLOAD_LOCATION'.length, limit: 260 }}
let:message
>
<span class="font-semibold text-immich-primary dark:text-immich-dark-primary">{message}</span>
{#snippet children({ message })}
<span class="font-semibold text-immich-primary dark:text-immich-dark-primary">{message}</span>
{/snippet}
</FormatMessage>
</p>
<p class="text-sm">
<FormatMessage
key="admin.storage_template_user_label"
values={{ label: $user.storageLabel || $user.id }}
let:message
>
<code class="text-immich-primary dark:text-immich-dark-primary">{message}</code>
<FormatMessage key="admin.storage_template_user_label" values={{ label: $user.storageLabel || $user.id }}>
{#snippet children({ message })}
<code class="text-immich-primary dark:text-immich-dark-primary">{message}</code>
{/snippet}
</FormatMessage>
</p>
@ -186,24 +207,30 @@
>/{parsedTemplate()}.jpg
</p>
<form autocomplete="off" class="flex flex-col" on:submit|preventDefault>
<form autocomplete="off" class="flex flex-col" onsubmit={preventDefault(bubble('submit'))}>
<div class="flex flex-col my-2">
<label class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm" for="preset-select">
{$t('preset')}
</label>
<select
class="immich-form-input p-2 mt-2 text-sm rounded-lg bg-slate-200 hover:cursor-pointer dark:bg-gray-600"
disabled={disabled || !config.storageTemplate.enabled}
name="presets"
id="preset-select"
bind:value={selectedPreset}
on:change={handlePresetSelection}
>
{#each templateOptions.presetOptions as preset}
<option value={preset}>{renderTemplate(preset)}</option>
{/each}
</select>
{#if templateOptions}
<label
class="font-medium text-immich-primary dark:text-immich-dark-primary text-sm"
for="preset-select"
>
{$t('preset')}
</label>
<select
class="immich-form-input p-2 mt-2 text-sm rounded-lg bg-slate-200 hover:cursor-pointer dark:bg-gray-600"
disabled={disabled || !config.storageTemplate.enabled}
name="presets"
id="preset-select"
bind:value={selectedPreset}
onchange={handlePresetSelection}
>
{#each templateOptions.presetOptions as preset}
<option value={preset}>{renderTemplate(preset)}</option>
{/each}
</select>
{/if}
</div>
<div class="flex gap-2 align-bottom">
<SettingInputField
label={$t('template')}
@ -232,11 +259,12 @@
<FormatMessage
key="admin.storage_template_migration_info"
values={{ job: $t('admin.storage_template_migration_job') }}
let:message
>
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary">
{message}
</a>
{#snippet children({ message })}
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary">
{message}
</a>
{/snippet}
</FormatMessage>
</p>
</section>
@ -247,7 +275,7 @@
{/if}
{#if minified}
<slot />
{@render children?.()}
{:else}
<SettingButtonsRow
onReset={(options) => onReset({ ...options, configKeys: ['storageTemplate'] })}

View file

@ -4,7 +4,11 @@
import { DateTime } from 'luxon';
import { t } from 'svelte-i18n';
export let options: SystemConfigTemplateStorageOptionDto;
interface Props {
options: SystemConfigTemplateStorageOptionDto;
}
let { options }: Props = $props();
const getLuxonExample = (format: string) => {
return DateTime.fromISO('2022-09-04T20:03:05.250Z', { locale: $locale }).toFormat(format);

View file

@ -7,22 +7,30 @@
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import { t } from 'svelte-i18n';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false;
export let onReset: SettingsResetEvent;
export let onSave: SettingsSaveEvent;
interface Props {
savedConfig: SystemConfigDto;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
const onsubmit = (event: Event) => {
event.preventDefault();
};
</script>
<div>
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<form autocomplete="off" {onsubmit}>
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingTextarea
{disabled}
label={$t('admin.theme_custom_css_settings')}
desc={$t('admin.theme_custom_css_settings_description')}
description={$t('admin.theme_custom_css_settings_description')}
bind:value={config.theme.customCss}
required={true}
isEdited={config.theme.customCss !== savedConfig.theme.customCss}

View file

@ -4,23 +4,30 @@
import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import { t } from 'svelte-i18n';
import { SettingInputFieldType } from '$lib/constants';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false;
export let onReset: SettingsResetEvent;
export let onSave: SettingsSaveEvent;
interface Props {
savedConfig: SystemConfigDto;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
const onsubmit = (event: Event) => {
event.preventDefault();
};
</script>
<div>
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<form autocomplete="off" {onsubmit}>
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingSwitch title={$t('admin.trash_enabled_description')} {disabled} bind:checked={config.trash.enabled} />
@ -29,7 +36,7 @@
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.trash_number_of_days')}
desc={$t('admin.trash_number_of_days_description')}
description={$t('admin.trash_number_of_days_description')}
bind:value={config.trash.days}
required={true}
disabled={disabled || !config.trash.enabled}

View file

@ -5,28 +5,31 @@
import type { SettingsResetEvent, SettingsSaveEvent } from '../admin-settings';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
import SettingInputField, {
SettingInputFieldType,
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import { t } from 'svelte-i18n';
import { SettingInputFieldType } from '$lib/constants';
export let savedConfig: SystemConfigDto;
export let defaultConfig: SystemConfigDto;
export let config: SystemConfigDto; // this is the config that is being edited
export let disabled = false;
export let onReset: SettingsResetEvent;
export let onSave: SettingsSaveEvent;
interface Props {
savedConfig: SystemConfigDto;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
</script>
<div>
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<form autocomplete="off" onsubmit={(e) => e.preventDefault()}>
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
min={1}
label={$t('admin.user_delete_delay_settings')}
desc={$t('admin.user_delete_delay_settings_description')}
description={$t('admin.user_delete_delay_settings_description')}
bind:value={config.user.deleteDelay}
isEdited={config.user.deleteDelay !== savedConfig.user.deleteDelay}
/>