refactor: admin settings (#23843)

This commit is contained in:
Daniel Dietzler 2025-11-13 19:17:44 +01:00 committed by GitHub
parent f73ca9d9c0
commit f1f203719d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
65 changed files with 787 additions and 1036 deletions

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { retrieveServerConfig } from '$lib/stores/server-config.store'; import { retrieveServerConfig } from '$lib/stores/system-config-manager.svelte';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { getConfig, getConfigDefaults, updateConfig, type SystemConfigDto } from '@immich/sdk'; import { getConfig, getConfigDefaults, updateConfig, type SystemConfigDto } from '@immich/sdk';
import { toastManager } from '@immich/ui'; import { toastManager } from '@immich/ui';

View File

@ -1,50 +1,43 @@
<script lang="ts"> <script lang="ts">
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.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 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 SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import { SettingInputFieldType } from '$lib/constants'; import { SettingInputFieldType } from '$lib/constants';
import FormatMessage from '$lib/elements/FormatMessage.svelte'; import FormatMessage from '$lib/elements/FormatMessage.svelte';
import AuthDisableLoginConfirmModal from '$lib/modals/AuthDisableLoginConfirmModal.svelte'; import AuthDisableLoginConfirmModal from '$lib/modals/AuthDisableLoginConfirmModal.svelte';
import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { OAuthTokenEndpointAuthMethod, unlinkAllOAuthAccountsAdmin, type SystemConfigDto } from '@immich/sdk'; import { OAuthTokenEndpointAuthMethod, unlinkAllOAuthAccountsAdmin } from '@immich/sdk';
import { Button, modalManager, Text, toastManager } from '@immich/ui'; import { Button, modalManager, Text, toastManager } from '@immich/ui';
import { mdiRestart } from '@mdi/js'; import { mdiRestart } from '@mdi/js';
import { isEqual } from 'lodash-es';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from './admin-settings';
interface Props { const disabled = $featureFlags.configFile;
savedConfig: SystemConfigDto; const config = $derived(systemConfigManager.value);
defaultConfig: SystemConfigDto; let configToEdit = $state(systemConfigManager.cloneValue());
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
const handleToggleOverride = () => { const handleToggleOverride = () => {
// click runs before bind // click runs before bind
const previouslyEnabled = config.oauth.mobileOverrideEnabled; const previouslyEnabled = configToEdit.oauth.mobileOverrideEnabled;
if (!previouslyEnabled && !config.oauth.mobileRedirectUri) { if (!previouslyEnabled && !configToEdit.oauth.mobileRedirectUri) {
config.oauth.mobileRedirectUri = globalThis.location.origin + '/api/oauth/mobile-redirect'; configToEdit.oauth.mobileRedirectUri = globalThis.location.origin + '/api/oauth/mobile-redirect';
} }
}; };
const handleSave = async (skipConfirm: boolean) => { const onBeforeSave = async () => {
const allMethodsDisabled = !config.oauth.enabled && !config.passwordLogin.enabled; const allMethodsDisabled = !configToEdit.oauth.enabled && !configToEdit.passwordLogin.enabled;
if (allMethodsDisabled && !skipConfirm) {
if (allMethodsDisabled) {
const isConfirmed = await modalManager.show(AuthDisableLoginConfirmModal); const isConfirmed = await modalManager.show(AuthDisableLoginConfirmModal);
if (!isConfirmed) { if (!isConfirmed) {
return; return false;
} }
} }
onSave({ passwordLogin: config.passwordLogin, oauth: config.oauth }); return true;
}; };
const handleUnlinkAllOAuthAccounts = async () => { const handleUnlinkAllOAuthAccounts = async () => {
@ -60,8 +53,8 @@
} }
try { try {
await unlinkAllOAuthAccountsAdmin({}); await unlinkAllOAuthAccountsAdmin();
toastManager.success({}); toastManager.success();
} catch (error) { } catch (error) {
handleError(error, $t('errors.something_went_wrong')); handleError(error, $t('errors.something_went_wrong'));
} }
@ -96,10 +89,10 @@
<SettingSwitch <SettingSwitch
{disabled} {disabled}
title={$t('admin.oauth_enable_description')} title={$t('admin.oauth_enable_description')}
bind:checked={config.oauth.enabled} bind:checked={configToEdit.oauth.enabled}
/> />
{#if config.oauth.enabled} {#if configToEdit.oauth.enabled}
<hr /> <hr />
<div class="flex items-center gap-2 justify-between"> <div class="flex items-center gap-2 justify-between">
@ -112,36 +105,36 @@
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label="ISSUER_URL" label="ISSUER_URL"
bind:value={config.oauth.issuerUrl} bind:value={configToEdit.oauth.issuerUrl}
required={true} required={true}
disabled={disabled || !config.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(config.oauth.issuerUrl == savedConfig.oauth.issuerUrl)} isEdited={!(configToEdit.oauth.issuerUrl === config.oauth.issuerUrl)}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label="CLIENT_ID" label="CLIENT_ID"
bind:value={config.oauth.clientId} bind:value={configToEdit.oauth.clientId}
required={true} required={true}
disabled={disabled || !config.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(config.oauth.clientId == savedConfig.oauth.clientId)} isEdited={!(configToEdit.oauth.clientId === config.oauth.clientId)}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label="CLIENT_SECRET" label="CLIENT_SECRET"
description={$t('admin.oauth_client_secret_description')} description={$t('admin.oauth_client_secret_description')}
bind:value={config.oauth.clientSecret} bind:value={configToEdit.oauth.clientSecret}
disabled={disabled || !config.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(config.oauth.clientSecret == savedConfig.oauth.clientSecret)} isEdited={!(configToEdit.oauth.clientSecret === config.oauth.clientSecret)}
/> />
{#if config.oauth.clientSecret} {#if configToEdit.oauth.clientSecret}
<SettingSelect <SettingSelect
label="TOKEN_ENDPOINT_AUTH_METHOD" label="TOKEN_ENDPOINT_AUTH_METHOD"
bind:value={config.oauth.tokenEndpointAuthMethod} bind:value={configToEdit.oauth.tokenEndpointAuthMethod}
disabled={disabled || !config.oauth.enabled || !config.oauth.clientSecret} disabled={disabled || !configToEdit.oauth.enabled || !configToEdit.oauth.clientSecret}
isEdited={!(config.oauth.tokenEndpointAuthMethod == savedConfig.oauth.tokenEndpointAuthMethod)} isEdited={!(configToEdit.oauth.tokenEndpointAuthMethod === config.oauth.tokenEndpointAuthMethod)}
options={[ options={[
{ value: OAuthTokenEndpointAuthMethod.ClientSecretPost, text: 'client_secret_post' }, { value: OAuthTokenEndpointAuthMethod.ClientSecretPost, text: 'client_secret_post' },
{ value: OAuthTokenEndpointAuthMethod.ClientSecretBasic, text: 'client_secret_basic' }, { value: OAuthTokenEndpointAuthMethod.ClientSecretBasic, text: 'client_secret_basic' },
@ -153,28 +146,28 @@
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label="SCOPE" label="SCOPE"
bind:value={config.oauth.scope} bind:value={configToEdit.oauth.scope}
required={true} required={true}
disabled={disabled || !config.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(config.oauth.scope == savedConfig.oauth.scope)} isEdited={!(configToEdit.oauth.scope === config.oauth.scope)}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label="ID_TOKEN_SIGNED_RESPONSE_ALG" label="ID_TOKEN_SIGNED_RESPONSE_ALG"
bind:value={config.oauth.signingAlgorithm} bind:value={configToEdit.oauth.signingAlgorithm}
required={true} required={true}
disabled={disabled || !config.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(config.oauth.signingAlgorithm == savedConfig.oauth.signingAlgorithm)} isEdited={!(configToEdit.oauth.signingAlgorithm === config.oauth.signingAlgorithm)}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label="USERINFO_SIGNED_RESPONSE_ALG" label="USERINFO_SIGNED_RESPONSE_ALG"
bind:value={config.oauth.profileSigningAlgorithm} bind:value={configToEdit.oauth.profileSigningAlgorithm}
required={true} required={true}
disabled={disabled || !config.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(config.oauth.profileSigningAlgorithm == savedConfig.oauth.profileSigningAlgorithm)} isEdited={!(configToEdit.oauth.profileSigningAlgorithm === config.oauth.profileSigningAlgorithm)}
/> />
<SettingInputField <SettingInputField
@ -182,72 +175,72 @@
label={$t('admin.oauth_timeout')} label={$t('admin.oauth_timeout')}
description={$t('admin.oauth_timeout_description')} description={$t('admin.oauth_timeout_description')}
required={true} required={true}
bind:value={config.oauth.timeout} bind:value={configToEdit.oauth.timeout}
disabled={disabled || !config.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(config.oauth.timeout == savedConfig.oauth.timeout)} isEdited={!(configToEdit.oauth.timeout === config.oauth.timeout)}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label={$t('admin.oauth_storage_label_claim')} label={$t('admin.oauth_storage_label_claim')}
description={$t('admin.oauth_storage_label_claim_description')} description={$t('admin.oauth_storage_label_claim_description')}
bind:value={config.oauth.storageLabelClaim} bind:value={configToEdit.oauth.storageLabelClaim}
required={true} required={true}
disabled={disabled || !config.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(config.oauth.storageLabelClaim == savedConfig.oauth.storageLabelClaim)} isEdited={!(configToEdit.oauth.storageLabelClaim === config.oauth.storageLabelClaim)}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label={$t('admin.oauth_role_claim')} label={$t('admin.oauth_role_claim')}
description={$t('admin.oauth_role_claim_description')} description={$t('admin.oauth_role_claim_description')}
bind:value={config.oauth.roleClaim} bind:value={configToEdit.oauth.roleClaim}
required={true} required={true}
disabled={disabled || !config.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(config.oauth.roleClaim == savedConfig.oauth.roleClaim)} isEdited={!(configToEdit.oauth.roleClaim === config.oauth.roleClaim)}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label={$t('admin.oauth_storage_quota_claim')} label={$t('admin.oauth_storage_quota_claim')}
description={$t('admin.oauth_storage_quota_claim_description')} description={$t('admin.oauth_storage_quota_claim_description')}
bind:value={config.oauth.storageQuotaClaim} bind:value={configToEdit.oauth.storageQuotaClaim}
required={true} required={true}
disabled={disabled || !config.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(config.oauth.storageQuotaClaim == savedConfig.oauth.storageQuotaClaim)} isEdited={!(configToEdit.oauth.storageQuotaClaim === config.oauth.storageQuotaClaim)}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
label={$t('admin.oauth_storage_quota_default')} label={$t('admin.oauth_storage_quota_default')}
description={$t('admin.oauth_storage_quota_default_description')} description={$t('admin.oauth_storage_quota_default_description')}
bind:value={config.oauth.defaultStorageQuota} bind:value={configToEdit.oauth.defaultStorageQuota}
required={false} required={false}
disabled={disabled || !config.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(config.oauth.defaultStorageQuota == savedConfig.oauth.defaultStorageQuota)} isEdited={!(configToEdit.oauth.defaultStorageQuota === config.oauth.defaultStorageQuota)}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label={$t('admin.oauth_button_text')} label={$t('admin.oauth_button_text')}
bind:value={config.oauth.buttonText} bind:value={configToEdit.oauth.buttonText}
required={false} required={false}
disabled={disabled || !config.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(config.oauth.buttonText == savedConfig.oauth.buttonText)} isEdited={!(configToEdit.oauth.buttonText === config.oauth.buttonText)}
/> />
<SettingSwitch <SettingSwitch
title={$t('admin.oauth_auto_register')} title={$t('admin.oauth_auto_register')}
subtitle={$t('admin.oauth_auto_register_description')} subtitle={$t('admin.oauth_auto_register_description')}
bind:checked={config.oauth.autoRegister} bind:checked={configToEdit.oauth.autoRegister}
disabled={disabled || !config.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
/> />
<SettingSwitch <SettingSwitch
title={$t('admin.oauth_auto_launch')} title={$t('admin.oauth_auto_launch')}
subtitle={$t('admin.oauth_auto_launch_description')} subtitle={$t('admin.oauth_auto_launch_description')}
disabled={disabled || !config.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
bind:checked={config.oauth.autoLaunch} bind:checked={configToEdit.oauth.autoLaunch}
/> />
<SettingSwitch <SettingSwitch
@ -255,19 +248,19 @@
subtitle={$t('admin.oauth_mobile_redirect_uri_override_description', { subtitle={$t('admin.oauth_mobile_redirect_uri_override_description', {
values: { callback: 'app.immich:///oauth-callback' }, values: { callback: 'app.immich:///oauth-callback' },
})} })}
disabled={disabled || !config.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
onToggle={() => handleToggleOverride()} onToggle={() => handleToggleOverride()}
bind:checked={config.oauth.mobileOverrideEnabled} bind:checked={configToEdit.oauth.mobileOverrideEnabled}
/> />
{#if config.oauth.mobileOverrideEnabled} {#if configToEdit.oauth.mobileOverrideEnabled}
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label={$t('admin.oauth_mobile_redirect_uri')} label={$t('admin.oauth_mobile_redirect_uri')}
bind:value={config.oauth.mobileRedirectUri} bind:value={configToEdit.oauth.mobileRedirectUri}
required={true} required={true}
disabled={disabled || !config.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(config.oauth.mobileRedirectUri == savedConfig.oauth.mobileRedirectUri)} isEdited={!(configToEdit.oauth.mobileRedirectUri === config.oauth.mobileRedirectUri)}
/> />
{/if} {/if}
{/if} {/if}
@ -284,19 +277,13 @@
<SettingSwitch <SettingSwitch
title={$t('admin.password_enable_description')} title={$t('admin.password_enable_description')}
{disabled} {disabled}
bind:checked={config.passwordLogin.enabled} bind:checked={configToEdit.passwordLogin.enabled}
/> />
</div> </div>
</div> </div>
</SettingAccordion> </SettingAccordion>
<SettingButtonsRow <SettingButtonsRow bind:configToEdit keys={['passwordLogin', 'oauth']} {onBeforeSave} {disabled} />
showResetToDefault={!isEqual(savedConfig.passwordLogin, defaultConfig.passwordLogin) ||
!isEqual(savedConfig.oauth, defaultConfig.oauth)}
{disabled}
onReset={(options) => onReset({ ...options, configKeys: ['passwordLogin', 'oauth'] })}
onSave={() => handleSave(false)}
/>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,26 +1,17 @@
<script lang="ts"> <script lang="ts">
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte'; import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingInputField 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 SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { SettingInputFieldType } from '$lib/constants'; import { SettingInputFieldType } from '$lib/constants';
import FormatMessage from '$lib/elements/FormatMessage.svelte'; import FormatMessage from '$lib/elements/FormatMessage.svelte';
import type { SystemConfigDto } from '@immich/sdk'; import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { isEqual } from 'lodash-es';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from './admin-settings';
interface Props { const disabled = $featureFlags.configFile;
savedConfig: SystemConfigDto; const config = $derived(systemConfigManager.value);
defaultConfig: SystemConfigDto; let configToEdit = $state(systemConfigManager.cloneValue());
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
let cronExpressionOptions = $derived([ let cronExpressionOptions = $derived([
{ text: $t('interval.night_at_midnight'), value: '0 0 * * *' }, { text: $t('interval.night_at_midnight'), value: '0 0 * * *' },
@ -28,44 +19,40 @@
{ text: $t('interval.day_at_onepm'), value: '0 13 * * *' }, { text: $t('interval.day_at_onepm'), value: '0 13 * * *' },
{ text: $t('interval.hours', { values: { hours: 6 } }), value: '0 */6 * * *' }, { text: $t('interval.hours', { values: { hours: 6 } }), value: '0 */6 * * *' },
]); ]);
const onsubmit = (event: Event) => {
event.preventDefault();
};
</script> </script>
<div> <div>
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<form autocomplete="off" {onsubmit}> <form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
<div class="ms-4 mt-4 flex flex-col gap-4"> <div class="ms-4 mt-4 flex flex-col gap-4">
<SettingSwitch <SettingSwitch
title={$t('admin.backup_database_enable_description')} title={$t('admin.backup_database_enable_description')}
{disabled} {disabled}
bind:checked={config.backup.database.enabled} bind:checked={configToEdit.backup.database.enabled}
/> />
<SettingSelect <SettingSelect
options={cronExpressionOptions} options={cronExpressionOptions}
disabled={disabled || !config.backup.database.enabled} disabled={disabled || !configToEdit.backup.database.enabled}
name="expression" name="expression"
label={$t('admin.cron_expression_presets')} label={$t('admin.cron_expression_presets')}
bind:value={config.backup.database.cronExpression} bind:value={configToEdit.backup.database.cronExpression}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
required={true} required={true}
disabled={disabled || !config.backup.database.enabled} disabled={disabled || !configToEdit.backup.database.enabled}
label={$t('admin.cron_expression')} label={$t('admin.cron_expression')}
bind:value={config.backup.database.cronExpression} bind:value={configToEdit.backup.database.cronExpression}
isEdited={config.backup.database.cronExpression !== savedConfig.backup.database.cronExpression} isEdited={configToEdit.backup.database.cronExpression !== config.backup.database.cronExpression}
> >
{#snippet descriptionSnippet()} {#snippet descriptionSnippet()}
<p class="text-sm dark:text-immich-dark-fg"> <p class="text-sm dark:text-immich-dark-fg">
<FormatMessage key="admin.cron_expression_description"> <FormatMessage key="admin.cron_expression_description">
{#snippet children({ message })} {#snippet children({ message })}
<a <a
href="https://crontab.guru/#{config.backup.database.cronExpression.replaceAll(' ', '_')}" href="https://crontab.guru/#{configToEdit.backup.database.cronExpression.replaceAll(' ', '_')}"
class="underline" class="underline"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
@ -83,17 +70,12 @@
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
required={true} required={true}
label={$t('admin.backup_keep_last_amount')} label={$t('admin.backup_keep_last_amount')}
disabled={disabled || !config.backup.database.enabled} disabled={disabled || !configToEdit.backup.database.enabled}
bind:value={config.backup.database.keepLastAmount} bind:value={configToEdit.backup.database.keepLastAmount}
isEdited={config.backup.database.keepLastAmount !== savedConfig.backup.database.keepLastAmount} isEdited={configToEdit.backup.database.keepLastAmount !== config.backup.database.keepLastAmount}
/> />
<SettingButtonsRow <SettingButtonsRow {disabled} bind:configToEdit keys={['backup']} />
onReset={(options) => onReset({ ...options, configKeys: ['backup'] })}
onSave={() => onSave({ backup: config.backup })}
showResetToDefault={!isEqual(savedConfig.backup, defaultConfig.backup)}
{disabled}
/>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,12 +1,13 @@
<script lang="ts"> <script lang="ts">
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.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 SettingCheckboxes from '$lib/components/shared-components/settings/setting-checkboxes.svelte'; import SettingCheckboxes from '$lib/components/shared-components/settings/setting-checkboxes.svelte';
import SettingInputField 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 SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import { SettingInputFieldType } from '$lib/constants'; import { SettingInputFieldType } from '$lib/constants';
import FormatMessage from '$lib/elements/FormatMessage.svelte'; import FormatMessage from '$lib/elements/FormatMessage.svelte';
import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { import {
AudioCodec, AudioCodec,
CQMode, CQMode,
@ -15,34 +16,21 @@
TranscodePolicy, TranscodePolicy,
VideoCodec, VideoCodec,
VideoContainer, VideoContainer,
type SystemConfigDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { Icon } from '@immich/ui'; import { Icon } from '@immich/ui';
import { mdiHelpCircleOutline } from '@mdi/js'; import { mdiHelpCircleOutline } from '@mdi/js';
import { isEqual, sortBy } from 'lodash-es'; import { isEqual, sortBy } from 'lodash-es';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from './admin-settings';
interface Props { const disabled = $featureFlags.configFile;
savedConfig: SystemConfigDto; const config = $derived(systemConfigManager.value);
defaultConfig: SystemConfigDto; let configToEdit = $state(systemConfigManager.cloneValue());
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> </script>
<div> <div>
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<form autocomplete="off" {onsubmit}> <form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
<div class="ms-4 mt-4 flex flex-col gap-4"> <div class="ms-4 mt-4 flex flex-col gap-4">
<p class="text-sm dark:text-immich-dark-fg"> <p class="text-sm dark:text-immich-dark-fg">
<Icon icon={mdiHelpCircleOutline} class="inline" size="15" /> <Icon icon={mdiHelpCircleOutline} class="inline" size="15" />
@ -75,7 +63,7 @@
label={$t('admin.transcoding_transcode_policy')} label={$t('admin.transcoding_transcode_policy')}
{disabled} {disabled}
desc={$t('admin.transcoding_transcode_policy_description')} desc={$t('admin.transcoding_transcode_policy_description')}
bind:value={config.ffmpeg.transcode} bind:value={configToEdit.ffmpeg.transcode}
name="transcode" name="transcode"
options={[ options={[
{ value: TranscodePolicy.All, text: $t('all_videos') }, { value: TranscodePolicy.All, text: $t('all_videos') },
@ -96,14 +84,14 @@
text: $t('admin.transcoding_disabled_description'), text: $t('admin.transcoding_disabled_description'),
}, },
]} ]}
isEdited={config.ffmpeg.transcode !== savedConfig.ffmpeg.transcode} isEdited={configToEdit.ffmpeg.transcode !== config.ffmpeg.transcode}
/> />
<SettingCheckboxes <SettingCheckboxes
label={$t('admin.transcoding_accepted_video_codecs')} label={$t('admin.transcoding_accepted_video_codecs')}
{disabled} {disabled}
desc={$t('admin.transcoding_accepted_video_codecs_description')} desc={$t('admin.transcoding_accepted_video_codecs_description')}
bind:value={config.ffmpeg.acceptedVideoCodecs} bind:value={configToEdit.ffmpeg.acceptedVideoCodecs}
name="videoCodecs" name="videoCodecs"
options={[ options={[
{ value: VideoCodec.H264, text: 'H.264' }, { value: VideoCodec.H264, text: 'H.264' },
@ -112,8 +100,8 @@
{ value: VideoCodec.Av1, text: 'AV1' }, { value: VideoCodec.Av1, text: 'AV1' },
]} ]}
isEdited={!isEqual( isEdited={!isEqual(
sortBy(configToEdit.ffmpeg.acceptedVideoCodecs),
sortBy(config.ffmpeg.acceptedVideoCodecs), sortBy(config.ffmpeg.acceptedVideoCodecs),
sortBy(savedConfig.ffmpeg.acceptedVideoCodecs),
)} )}
/> />
@ -121,7 +109,7 @@
label={$t('admin.transcoding_accepted_audio_codecs')} label={$t('admin.transcoding_accepted_audio_codecs')}
{disabled} {disabled}
desc={$t('admin.transcoding_accepted_audio_codecs_description')} desc={$t('admin.transcoding_accepted_audio_codecs_description')}
bind:value={config.ffmpeg.acceptedAudioCodecs} bind:value={configToEdit.ffmpeg.acceptedAudioCodecs}
name="audioCodecs" name="audioCodecs"
options={[ options={[
{ value: AudioCodec.Aac, text: 'AAC' }, { value: AudioCodec.Aac, text: 'AAC' },
@ -130,8 +118,8 @@
{ value: AudioCodec.PcmS16Le, text: 'PCM (16 bit)' }, { value: AudioCodec.PcmS16Le, text: 'PCM (16 bit)' },
]} ]}
isEdited={!isEqual( isEdited={!isEqual(
sortBy(configToEdit.ffmpeg.acceptedAudioCodecs),
sortBy(config.ffmpeg.acceptedAudioCodecs), sortBy(config.ffmpeg.acceptedAudioCodecs),
sortBy(savedConfig.ffmpeg.acceptedAudioCodecs),
)} )}
/> />
@ -139,7 +127,7 @@
label={$t('admin.transcoding_accepted_containers')} label={$t('admin.transcoding_accepted_containers')}
{disabled} {disabled}
desc={$t('admin.transcoding_accepted_containers_description')} desc={$t('admin.transcoding_accepted_containers_description')}
bind:value={config.ffmpeg.acceptedContainers} bind:value={configToEdit.ffmpeg.acceptedContainers}
name="videoContainers" name="videoContainers"
options={[ options={[
{ value: VideoContainer.Mov, text: 'MOV' }, { value: VideoContainer.Mov, text: 'MOV' },
@ -147,8 +135,8 @@
{ value: VideoContainer.Webm, text: 'WebM' }, { value: VideoContainer.Webm, text: 'WebM' },
]} ]}
isEdited={!isEqual( isEdited={!isEqual(
sortBy(configToEdit.ffmpeg.acceptedContainers),
sortBy(config.ffmpeg.acceptedContainers), sortBy(config.ffmpeg.acceptedContainers),
sortBy(savedConfig.ffmpeg.acceptedContainers),
)} )}
/> />
</div> </div>
@ -164,7 +152,7 @@
label={$t('admin.transcoding_video_codec')} label={$t('admin.transcoding_video_codec')}
{disabled} {disabled}
desc={$t('admin.transcoding_video_codec_description')} desc={$t('admin.transcoding_video_codec_description')}
bind:value={config.ffmpeg.targetVideoCodec} bind:value={configToEdit.ffmpeg.targetVideoCodec}
options={[ options={[
{ value: VideoCodec.H264, text: 'h264' }, { value: VideoCodec.H264, text: 'h264' },
{ value: VideoCodec.Hevc, text: 'hevc' }, { value: VideoCodec.Hevc, text: 'hevc' },
@ -172,8 +160,8 @@
{ value: VideoCodec.Av1, text: 'av1' }, { value: VideoCodec.Av1, text: 'av1' },
]} ]}
name="vcodec" name="vcodec"
isEdited={config.ffmpeg.targetVideoCodec !== savedConfig.ffmpeg.targetVideoCodec} isEdited={configToEdit.ffmpeg.targetVideoCodec !== config.ffmpeg.targetVideoCodec}
onSelect={() => (config.ffmpeg.acceptedVideoCodecs = [config.ffmpeg.targetVideoCodec])} onSelect={() => (configToEdit.ffmpeg.acceptedVideoCodecs = [configToEdit.ffmpeg.targetVideoCodec])}
/> />
<!-- PCM is excluded here since it's a bad choice for users storage-wise --> <!-- PCM is excluded here since it's a bad choice for users storage-wise -->
@ -181,25 +169,25 @@
label={$t('admin.transcoding_audio_codec')} label={$t('admin.transcoding_audio_codec')}
{disabled} {disabled}
desc={$t('admin.transcoding_audio_codec_description')} desc={$t('admin.transcoding_audio_codec_description')}
bind:value={config.ffmpeg.targetAudioCodec} bind:value={configToEdit.ffmpeg.targetAudioCodec}
options={[ options={[
{ value: AudioCodec.Aac, text: 'aac' }, { value: AudioCodec.Aac, text: 'aac' },
{ value: AudioCodec.Mp3, text: 'mp3' }, { value: AudioCodec.Mp3, text: 'mp3' },
{ value: AudioCodec.Libopus, text: 'opus' }, { value: AudioCodec.Libopus, text: 'opus' },
]} ]}
name="acodec" name="acodec"
isEdited={config.ffmpeg.targetAudioCodec !== savedConfig.ffmpeg.targetAudioCodec} isEdited={configToEdit.ffmpeg.targetAudioCodec !== config.ffmpeg.targetAudioCodec}
onSelect={() => onSelect={() =>
config.ffmpeg.acceptedAudioCodecs.includes(config.ffmpeg.targetAudioCodec) configToEdit.ffmpeg.acceptedAudioCodecs.includes(configToEdit.ffmpeg.targetAudioCodec)
? null ? null
: config.ffmpeg.acceptedAudioCodecs.push(config.ffmpeg.targetAudioCodec)} : configToEdit.ffmpeg.acceptedAudioCodecs.push(configToEdit.ffmpeg.targetAudioCodec)}
/> />
<SettingSelect <SettingSelect
label={$t('admin.transcoding_target_resolution')} label={$t('admin.transcoding_target_resolution')}
{disabled} {disabled}
desc={$t('admin.transcoding_target_resolution_description')} desc={$t('admin.transcoding_target_resolution_description')}
bind:value={config.ffmpeg.targetResolution} bind:value={configToEdit.ffmpeg.targetResolution}
options={[ options={[
{ value: '2160', text: '4k' }, { value: '2160', text: '4k' },
{ value: '1440', text: '1440p' }, { value: '1440', text: '1440p' },
@ -209,7 +197,7 @@
{ value: 'original', text: $t('original') }, { value: 'original', text: $t('original') },
]} ]}
name="resolution" name="resolution"
isEdited={config.ffmpeg.targetResolution !== savedConfig.ffmpeg.targetResolution} isEdited={configToEdit.ffmpeg.targetResolution !== config.ffmpeg.targetResolution}
/> />
<SettingInputField <SettingInputField
@ -217,16 +205,16 @@
{disabled} {disabled}
label={$t('admin.transcoding_constant_rate_factor')} label={$t('admin.transcoding_constant_rate_factor')}
description={$t('admin.transcoding_constant_rate_factor_description')} description={$t('admin.transcoding_constant_rate_factor_description')}
bind:value={config.ffmpeg.crf} bind:value={configToEdit.ffmpeg.crf}
required={true} required={true}
isEdited={config.ffmpeg.crf !== savedConfig.ffmpeg.crf} isEdited={configToEdit.ffmpeg.crf !== config.ffmpeg.crf}
/> />
<SettingSelect <SettingSelect
label={$t('admin.transcoding_preset_preset')} label={$t('admin.transcoding_preset_preset')}
{disabled} {disabled}
desc={$t('admin.transcoding_preset_preset_description')} desc={$t('admin.transcoding_preset_preset_description')}
bind:value={config.ffmpeg.preset} bind:value={configToEdit.ffmpeg.preset}
name="preset" name="preset"
options={[ options={[
{ value: 'ultrafast', text: 'ultrafast' }, { value: 'ultrafast', text: 'ultrafast' },
@ -239,7 +227,7 @@
{ value: 'slower', text: 'slower' }, { value: 'slower', text: 'slower' },
{ value: 'veryslow', text: 'veryslow' }, { value: 'veryslow', text: 'veryslow' },
]} ]}
isEdited={config.ffmpeg.preset !== savedConfig.ffmpeg.preset} isEdited={configToEdit.ffmpeg.preset !== config.ffmpeg.preset}
/> />
<SettingInputField <SettingInputField
@ -247,8 +235,8 @@
{disabled} {disabled}
label={$t('admin.transcoding_max_bitrate')} label={$t('admin.transcoding_max_bitrate')}
description={$t('admin.transcoding_max_bitrate_description')} description={$t('admin.transcoding_max_bitrate_description')}
bind:value={config.ffmpeg.maxBitrate} bind:value={configToEdit.ffmpeg.maxBitrate}
isEdited={config.ffmpeg.maxBitrate !== savedConfig.ffmpeg.maxBitrate} isEdited={configToEdit.ffmpeg.maxBitrate !== config.ffmpeg.maxBitrate}
/> />
<SettingInputField <SettingInputField
@ -256,15 +244,15 @@
{disabled} {disabled}
label={$t('admin.transcoding_threads')} label={$t('admin.transcoding_threads')}
description={$t('admin.transcoding_threads_description')} description={$t('admin.transcoding_threads_description')}
bind:value={config.ffmpeg.threads} bind:value={configToEdit.ffmpeg.threads}
isEdited={config.ffmpeg.threads !== savedConfig.ffmpeg.threads} isEdited={configToEdit.ffmpeg.threads !== config.ffmpeg.threads}
/> />
<SettingSelect <SettingSelect
label={$t('admin.transcoding_tone_mapping')} label={$t('admin.transcoding_tone_mapping')}
{disabled} {disabled}
desc={$t('admin.transcoding_tone_mapping_description')} desc={$t('admin.transcoding_tone_mapping_description')}
bind:value={config.ffmpeg.tonemap} bind:value={configToEdit.ffmpeg.tonemap}
name="tonemap" name="tonemap"
options={[ options={[
{ {
@ -284,15 +272,15 @@
text: $t('disabled'), text: $t('disabled'),
}, },
]} ]}
isEdited={config.ffmpeg.tonemap !== savedConfig.ffmpeg.tonemap} isEdited={configToEdit.ffmpeg.tonemap !== config.ffmpeg.tonemap}
/> />
<SettingSwitch <SettingSwitch
title={$t('admin.transcoding_two_pass_encoding')} title={$t('admin.transcoding_two_pass_encoding')}
{disabled} {disabled}
subtitle={$t('admin.transcoding_two_pass_encoding_setting_description')} subtitle={$t('admin.transcoding_two_pass_encoding_setting_description')}
bind:checked={config.ffmpeg.twoPass} bind:checked={configToEdit.ffmpeg.twoPass}
isEdited={config.ffmpeg.twoPass !== savedConfig.ffmpeg.twoPass} isEdited={configToEdit.ffmpeg.twoPass !== config.ffmpeg.twoPass}
/> />
</div> </div>
</SettingAccordion> </SettingAccordion>
@ -307,7 +295,7 @@
label={$t('admin.transcoding_acceleration_api')} label={$t('admin.transcoding_acceleration_api')}
{disabled} {disabled}
desc={$t('admin.transcoding_acceleration_api_description')} desc={$t('admin.transcoding_acceleration_api_description')}
bind:value={config.ffmpeg.accel} bind:value={configToEdit.ffmpeg.accel}
name="accel" name="accel"
options={[ options={[
{ value: TranscodeHWAccel.Nvenc, text: $t('admin.transcoding_acceleration_nvenc') }, { value: TranscodeHWAccel.Nvenc, text: $t('admin.transcoding_acceleration_nvenc') },
@ -328,27 +316,27 @@
text: $t('disabled'), text: $t('disabled'),
}, },
]} ]}
isEdited={config.ffmpeg.accel !== savedConfig.ffmpeg.accel} isEdited={configToEdit.ffmpeg.accel !== config.ffmpeg.accel}
/> />
<SettingSwitch <SettingSwitch
title={$t('admin.transcoding_hardware_decoding')} title={$t('admin.transcoding_hardware_decoding')}
{disabled} {disabled}
subtitle={$t('admin.transcoding_hardware_decoding_setting_description')} subtitle={$t('admin.transcoding_hardware_decoding_setting_description')}
bind:checked={config.ffmpeg.accelDecode} bind:checked={configToEdit.ffmpeg.accelDecode}
isEdited={config.ffmpeg.accelDecode !== savedConfig.ffmpeg.accelDecode} isEdited={configToEdit.ffmpeg.accelDecode !== config.ffmpeg.accelDecode}
/> />
<SettingSelect <SettingSelect
label={$t('admin.transcoding_constant_quality_mode')} label={$t('admin.transcoding_constant_quality_mode')}
desc={$t('admin.transcoding_constant_quality_mode_description')} desc={$t('admin.transcoding_constant_quality_mode_description')}
bind:value={config.ffmpeg.cqMode} bind:value={configToEdit.ffmpeg.cqMode}
options={[ options={[
{ value: CQMode.Auto, text: 'Auto' }, { value: CQMode.Auto, text: 'Auto' },
{ value: CQMode.Icq, text: 'ICQ' }, { value: CQMode.Icq, text: 'ICQ' },
{ value: CQMode.Cqp, text: 'CQP' }, { value: CQMode.Cqp, text: 'CQP' },
]} ]}
isEdited={config.ffmpeg.cqMode !== savedConfig.ffmpeg.cqMode} isEdited={configToEdit.ffmpeg.cqMode !== config.ffmpeg.cqMode}
{disabled} {disabled}
/> />
@ -356,16 +344,16 @@
title={$t('admin.transcoding_temporal_aq')} title={$t('admin.transcoding_temporal_aq')}
{disabled} {disabled}
subtitle={$t('admin.transcoding_temporal_aq_description')} subtitle={$t('admin.transcoding_temporal_aq_description')}
bind:checked={config.ffmpeg.temporalAQ} bind:checked={configToEdit.ffmpeg.temporalAQ}
isEdited={config.ffmpeg.temporalAQ !== savedConfig.ffmpeg.temporalAQ} isEdited={configToEdit.ffmpeg.temporalAQ !== config.ffmpeg.temporalAQ}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label={$t('admin.transcoding_preferred_hardware_device')} label={$t('admin.transcoding_preferred_hardware_device')}
description={$t('admin.transcoding_preferred_hardware_device_description')} description={$t('admin.transcoding_preferred_hardware_device_description')}
bind:value={config.ffmpeg.preferredHwDevice} bind:value={configToEdit.ffmpeg.preferredHwDevice}
isEdited={config.ffmpeg.preferredHwDevice !== savedConfig.ffmpeg.preferredHwDevice} isEdited={configToEdit.ffmpeg.preferredHwDevice !== config.ffmpeg.preferredHwDevice}
{disabled} {disabled}
/> />
</div> </div>
@ -381,8 +369,8 @@
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
label={$t('admin.transcoding_max_b_frames')} label={$t('admin.transcoding_max_b_frames')}
description={$t('admin.transcoding_max_b_frames_description')} description={$t('admin.transcoding_max_b_frames_description')}
bind:value={config.ffmpeg.bframes} bind:value={configToEdit.ffmpeg.bframes}
isEdited={config.ffmpeg.bframes !== savedConfig.ffmpeg.bframes} isEdited={configToEdit.ffmpeg.bframes !== config.ffmpeg.bframes}
{disabled} {disabled}
/> />
@ -390,8 +378,8 @@
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
label={$t('admin.transcoding_reference_frames')} label={$t('admin.transcoding_reference_frames')}
description={$t('admin.transcoding_reference_frames_description')} description={$t('admin.transcoding_reference_frames_description')}
bind:value={config.ffmpeg.refs} bind:value={configToEdit.ffmpeg.refs}
isEdited={config.ffmpeg.refs !== savedConfig.ffmpeg.refs} isEdited={configToEdit.ffmpeg.refs !== config.ffmpeg.refs}
{disabled} {disabled}
/> />
@ -399,8 +387,8 @@
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
label={$t('admin.transcoding_max_keyframe_interval')} label={$t('admin.transcoding_max_keyframe_interval')}
description={$t('admin.transcoding_max_keyframe_interval_description')} description={$t('admin.transcoding_max_keyframe_interval_description')}
bind:value={config.ffmpeg.gopSize} bind:value={configToEdit.ffmpeg.gopSize}
isEdited={config.ffmpeg.gopSize !== savedConfig.ffmpeg.gopSize} isEdited={configToEdit.ffmpeg.gopSize !== config.ffmpeg.gopSize}
{disabled} {disabled}
/> />
</div> </div>
@ -408,12 +396,7 @@
</div> </div>
<div class="ms-4"> <div class="ms-4">
<SettingButtonsRow <SettingButtonsRow bind:configToEdit keys={['ffmpeg']} {disabled} />
onReset={(options) => onReset({ ...options, configKeys: ['ffmpeg'] })}
onSave={() => onSave({ ffmpeg: config.ffmpeg })}
showResetToDefault={!isEqual(savedConfig.ffmpeg, defaultConfig.ffmpeg)}
{disabled}
/>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,62 +1,40 @@
<script lang="ts"> <script lang="ts">
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte'; import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import { Colorspace, ImageFormat, type SystemConfigDto } from '@immich/sdk'; import { Colorspace, ImageFormat } from '@immich/sdk';
import { isEqual } from 'lodash-es';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from './admin-settings';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.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 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 SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import { SettingInputFieldType } from '$lib/constants'; import { SettingInputFieldType } from '$lib/constants';
import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
interface Props { const disabled = $featureFlags.configFile;
savedConfig: SystemConfigDto; const config = $derived(systemConfigManager.value);
defaultConfig: SystemConfigDto; let configToEdit = $state(systemConfigManager.cloneValue());
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> </script>
<div> <div>
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<form autocomplete="off" {onsubmit}> <form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
<div class="ms-4 mt-4"> <div class="ms-4 mt-4">
<SettingAccordion <SettingAccordion
key="thumbnail-settings" key="thumbnail-settings"
title={$t('admin.image_thumbnail_title')} title={$t('admin.image_thumbnail_title')}
subtitle={$t('admin.image_thumbnail_description')} subtitle={$t('admin.image_thumbnail_description')}
isOpen={openByDefault}
> >
<SettingSelect <SettingSelect
label={$t('admin.image_format')} label={$t('admin.image_format')}
desc={$t('admin.image_format_description')} desc={$t('admin.image_format_description')}
bind:value={config.image.thumbnail.format} bind:value={configToEdit.image.thumbnail.format}
options={[ options={[
{ value: ImageFormat.Jpeg, text: 'JPEG' }, { value: ImageFormat.Jpeg, text: 'JPEG' },
{ value: ImageFormat.Webp, text: 'WebP' }, { value: ImageFormat.Webp, text: 'WebP' },
]} ]}
name="format" name="format"
isEdited={config.image.thumbnail.format !== savedConfig.image.thumbnail.format} isEdited={configToEdit.image.thumbnail.format !== config.image.thumbnail.format}
{disabled} {disabled}
/> />
@ -64,7 +42,7 @@
label={$t('admin.image_resolution')} label={$t('admin.image_resolution')}
desc={$t('admin.image_resolution_description')} desc={$t('admin.image_resolution_description')}
number number
bind:value={config.image.thumbnail.size} bind:value={configToEdit.image.thumbnail.size}
options={[ options={[
{ value: 1080, text: '1080p' }, { value: 1080, text: '1080p' },
{ value: 720, text: '720p' }, { value: 720, text: '720p' },
@ -73,7 +51,7 @@
{ value: 200, text: '200p' }, { value: 200, text: '200p' },
]} ]}
name="resolution" name="resolution"
isEdited={config.image.thumbnail.size !== savedConfig.image.thumbnail.size} isEdited={configToEdit.image.thumbnail.size !== config.image.thumbnail.size}
{disabled} {disabled}
/> />
@ -81,8 +59,8 @@
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
label={$t('admin.image_quality')} label={$t('admin.image_quality')}
description={$t('admin.image_thumbnail_quality_description')} description={$t('admin.image_thumbnail_quality_description')}
bind:value={config.image.thumbnail.quality} bind:value={configToEdit.image.thumbnail.quality}
isEdited={config.image.thumbnail.quality !== savedConfig.image.thumbnail.quality} isEdited={configToEdit.image.thumbnail.quality !== config.image.thumbnail.quality}
{disabled} {disabled}
/> />
</SettingAccordion> </SettingAccordion>
@ -91,18 +69,17 @@
key="preview-settings" key="preview-settings"
title={$t('admin.image_preview_title')} title={$t('admin.image_preview_title')}
subtitle={$t('admin.image_preview_description')} subtitle={$t('admin.image_preview_description')}
isOpen={openByDefault}
> >
<SettingSelect <SettingSelect
label={$t('admin.image_format')} label={$t('admin.image_format')}
desc={$t('admin.image_format_description')} desc={$t('admin.image_format_description')}
bind:value={config.image.preview.format} bind:value={configToEdit.image.preview.format}
options={[ options={[
{ value: ImageFormat.Jpeg, text: 'JPEG' }, { value: ImageFormat.Jpeg, text: 'JPEG' },
{ value: ImageFormat.Webp, text: 'WebP' }, { value: ImageFormat.Webp, text: 'WebP' },
]} ]}
name="format" name="format"
isEdited={config.image.preview.format !== savedConfig.image.preview.format} isEdited={configToEdit.image.preview.format !== config.image.preview.format}
{disabled} {disabled}
/> />
@ -110,7 +87,7 @@
label={$t('admin.image_resolution')} label={$t('admin.image_resolution')}
desc={$t('admin.image_resolution_description')} desc={$t('admin.image_resolution_description')}
number number
bind:value={config.image.preview.size} bind:value={configToEdit.image.preview.size}
options={[ options={[
{ value: 2160, text: '4K' }, { value: 2160, text: '4K' },
{ value: 1440, text: '1440p' }, { value: 1440, text: '1440p' },
@ -118,7 +95,7 @@
{ value: 720, text: '720p' }, { value: 720, text: '720p' },
]} ]}
name="resolution" name="resolution"
isEdited={config.image.preview.size !== savedConfig.image.preview.size} isEdited={configToEdit.image.preview.size !== config.image.preview.size}
{disabled} {disabled}
/> />
@ -126,8 +103,8 @@
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
label={$t('admin.image_quality')} label={$t('admin.image_quality')}
description={$t('admin.image_preview_quality_description')} description={$t('admin.image_preview_quality_description')}
bind:value={config.image.preview.quality} bind:value={configToEdit.image.preview.quality}
isEdited={config.image.preview.quality !== savedConfig.image.preview.quality} isEdited={configToEdit.image.preview.quality !== config.image.preview.quality}
{disabled} {disabled}
/> />
</SettingAccordion> </SettingAccordion>
@ -136,14 +113,13 @@
key="fullsize-settings" key="fullsize-settings"
title={$t('admin.image_fullsize_title')} title={$t('admin.image_fullsize_title')}
subtitle={$t('admin.image_fullsize_description')} subtitle={$t('admin.image_fullsize_description')}
isOpen={openByDefault}
> >
<SettingSwitch <SettingSwitch
title={$t('admin.image_fullsize_enabled')} title={$t('admin.image_fullsize_enabled')}
subtitle={$t('admin.image_fullsize_enabled_description')} subtitle={$t('admin.image_fullsize_enabled_description')}
checked={config.image.fullsize.enabled} checked={configToEdit.image.fullsize.enabled}
onToggle={(isChecked) => (config.image.fullsize.enabled = isChecked)} onToggle={(isChecked) => (configToEdit.image.fullsize.enabled = isChecked)}
isEdited={config.image.fullsize.enabled !== savedConfig.image.fullsize.enabled} isEdited={configToEdit.image.fullsize.enabled !== config.image.fullsize.enabled}
{disabled} {disabled}
/> />
@ -152,23 +128,23 @@
<SettingSelect <SettingSelect
label={$t('admin.image_format')} label={$t('admin.image_format')}
desc={$t('admin.image_format_description')} desc={$t('admin.image_format_description')}
bind:value={config.image.fullsize.format} bind:value={configToEdit.image.fullsize.format}
options={[ options={[
{ value: ImageFormat.Jpeg, text: 'JPEG' }, { value: ImageFormat.Jpeg, text: 'JPEG' },
{ value: ImageFormat.Webp, text: 'WebP' }, { value: ImageFormat.Webp, text: 'WebP' },
]} ]}
name="format" name="format"
isEdited={config.image.fullsize.format !== savedConfig.image.fullsize.format} isEdited={configToEdit.image.fullsize.format !== config.image.fullsize.format}
disabled={disabled || !config.image.fullsize.enabled} disabled={disabled || !configToEdit.image.fullsize.enabled}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
label={$t('admin.image_quality')} label={$t('admin.image_quality')}
description={$t('admin.image_fullsize_quality_description')} description={$t('admin.image_fullsize_quality_description')}
bind:value={config.image.fullsize.quality} bind:value={configToEdit.image.fullsize.quality}
isEdited={config.image.fullsize.quality !== savedConfig.image.fullsize.quality} isEdited={configToEdit.image.fullsize.quality !== config.image.fullsize.quality}
disabled={disabled || !config.image.fullsize.enabled} disabled={disabled || !configToEdit.image.fullsize.enabled}
/> />
</SettingAccordion> </SettingAccordion>
@ -176,9 +152,9 @@
<SettingSwitch <SettingSwitch
title={$t('admin.image_prefer_wide_gamut')} title={$t('admin.image_prefer_wide_gamut')}
subtitle={$t('admin.image_prefer_wide_gamut_setting_description')} subtitle={$t('admin.image_prefer_wide_gamut_setting_description')}
checked={config.image.colorspace === Colorspace.P3} checked={configToEdit.image.colorspace === Colorspace.P3}
onToggle={(isChecked) => (config.image.colorspace = isChecked ? Colorspace.P3 : Colorspace.Srgb)} onToggle={(isChecked) => (configToEdit.image.colorspace = isChecked ? Colorspace.P3 : Colorspace.Srgb)}
isEdited={config.image.colorspace !== savedConfig.image.colorspace} isEdited={configToEdit.image.colorspace !== config.image.colorspace}
{disabled} {disabled}
/> />
</div> </div>
@ -187,21 +163,16 @@
<SettingSwitch <SettingSwitch
title={$t('admin.image_prefer_embedded_preview')} title={$t('admin.image_prefer_embedded_preview')}
subtitle={$t('admin.image_prefer_embedded_preview_setting_description')} subtitle={$t('admin.image_prefer_embedded_preview_setting_description')}
checked={config.image.extractEmbedded} checked={configToEdit.image.extractEmbedded}
onToggle={() => (config.image.extractEmbedded = !config.image.extractEmbedded)} onToggle={() => (configToEdit.image.extractEmbedded = !configToEdit.image.extractEmbedded)}
isEdited={config.image.extractEmbedded !== savedConfig.image.extractEmbedded} isEdited={configToEdit.image.extractEmbedded !== config.image.extractEmbedded}
{disabled} {disabled}
/> />
</div> </div>
</div> </div>
<div class="ms-4 mt-4"> <div class="ms-4 mt-4">
<SettingButtonsRow <SettingButtonsRow bind:configToEdit keys={['image']} {disabled} />
onReset={(options) => onReset({ ...options, configKeys: ['image'] })}
onSave={() => onSave({ image: config.image })}
showResetToDefault={!isEqual(savedConfig.image, defaultConfig.image)}
{disabled}
/>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,24 +1,16 @@
<script lang="ts"> <script lang="ts">
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte'; import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte'; import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import { SettingInputFieldType } from '$lib/constants'; import { SettingInputFieldType } from '$lib/constants';
import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { getJobName } from '$lib/utils'; import { getJobName } from '$lib/utils';
import { JobName, type SystemConfigDto, type SystemConfigJobDto } from '@immich/sdk'; import { JobName, type SystemConfigJobDto } from '@immich/sdk';
import { isEqual } from 'lodash-es';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from './admin-settings';
interface Props { const disabled = $featureFlags.configFile;
savedConfig: SystemConfigDto; const config = $derived(systemConfigManager.value);
defaultConfig: SystemConfigDto; let configToEdit = $state(systemConfigManager.cloneValue());
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
const jobNames = [ const jobNames = [
JobName.ThumbnailGeneration, JobName.ThumbnailGeneration,
@ -34,19 +26,14 @@
JobName.Ocr, JobName.Ocr,
]; ];
// eslint-disable-next-line @typescript-eslint/no-explicit-any function isSystemConfigJobDto(jobName: string): jobName is keyof SystemConfigJobDto {
function isSystemConfigJobDto(jobName: any): jobName is keyof SystemConfigJobDto { return jobName in configToEdit.job;
return jobName in config.job;
} }
const onsubmit = (event: Event) => {
event.preventDefault();
};
</script> </script>
<div> <div>
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<form autocomplete="off" {onsubmit}> <form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
{#each jobNames as jobName (jobName)} {#each jobNames as jobName (jobName)}
<div class="ms-4 mt-4 flex flex-col gap-4"> <div class="ms-4 mt-4 flex flex-col gap-4">
{#if isSystemConfigJobDto(jobName)} {#if isSystemConfigJobDto(jobName)}
@ -55,9 +42,9 @@
{disabled} {disabled}
label={$t('admin.job_concurrency', { values: { job: $getJobName(jobName) } })} label={$t('admin.job_concurrency', { values: { job: $getJobName(jobName) } })}
description="" description=""
bind:value={config.job[jobName].concurrency} bind:value={configToEdit.job[jobName].concurrency}
required={true} required={true}
isEdited={!(config.job[jobName].concurrency == savedConfig.job[jobName].concurrency)} isEdited={!(configToEdit.job[jobName].concurrency == config.job[jobName].concurrency)}
/> />
{:else} {:else}
<SettingInputField <SettingInputField
@ -73,12 +60,7 @@
{/each} {/each}
<div class="ms-4"> <div class="ms-4">
<SettingButtonsRow <SettingButtonsRow bind:configToEdit keys={['job']} {disabled} />
onReset={(options) => onReset({ ...options, configKeys: ['job'] })}
onSave={() => onSave({ job: config.job })}
showResetToDefault={!isEqual(savedConfig.job, defaultConfig.job)}
{disabled}
/>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,36 +1,18 @@
<script lang="ts"> <script lang="ts">
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.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 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 SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import { SettingInputFieldType } from '$lib/constants'; import { SettingInputFieldType } from '$lib/constants';
import FormatMessage from '$lib/elements/FormatMessage.svelte'; import FormatMessage from '$lib/elements/FormatMessage.svelte';
import type { SystemConfigDto } from '@immich/sdk'; import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { isEqual } from 'lodash-es';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from './admin-settings';
interface Props { const disabled = $featureFlags.configFile;
savedConfig: SystemConfigDto; const config = $derived(systemConfigManager.value);
defaultConfig: SystemConfigDto; let configToEdit = $state(systemConfigManager.cloneValue());
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
openByDefault?: boolean;
}
let {
savedConfig,
defaultConfig,
config = $bindable(),
disabled = false,
onReset,
onSave,
openByDefault = false,
}: Props = $props();
let cronExpressionOptions = $derived([ let cronExpressionOptions = $derived([
{ text: $t('interval.night_at_midnight'), value: '0 0 * * *' }, { text: $t('interval.night_at_midnight'), value: '0 0 * * *' },
@ -38,27 +20,22 @@
{ text: $t('interval.day_at_onepm'), value: '0 13 * * *' }, { text: $t('interval.day_at_onepm'), value: '0 13 * * *' },
{ text: $t('interval.hours', { values: { hours: 6 } }), value: '0 */6 * * *' }, { text: $t('interval.hours', { values: { hours: 6 } }), value: '0 */6 * * *' },
]); ]);
const onsubmit = (event: Event) => {
event.preventDefault();
};
</script> </script>
<div> <div>
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<form autocomplete="off" {onsubmit}> <form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
<div class="ms-4 mt-4 flex flex-col gap-4"> <div class="ms-4 mt-4 flex flex-col gap-4">
<SettingAccordion <SettingAccordion
key="library-watching" key="library-watching"
title={$t('admin.library_watching_settings')} title={$t('admin.library_watching_settings')}
subtitle={$t('admin.library_watching_settings_description')} subtitle={$t('admin.library_watching_settings_description')}
isOpen={openByDefault}
> >
<div class="ms-4 mt-4 flex flex-col gap-4"> <div class="ms-4 mt-4 flex flex-col gap-4">
<SettingSwitch <SettingSwitch
title={$t('admin.library_watching_enable_description')} title={$t('admin.library_watching_enable_description')}
{disabled} {disabled}
bind:checked={config.library.watch.enabled} bind:checked={configToEdit.library.watch.enabled}
/> />
</div> </div>
</SettingAccordion> </SettingAccordion>
@ -67,37 +44,36 @@
key="library-scanning" key="library-scanning"
title={$t('admin.library_scanning')} title={$t('admin.library_scanning')}
subtitle={$t('admin.library_scanning_description')} subtitle={$t('admin.library_scanning_description')}
isOpen={openByDefault}
> >
<div class="ms-4 mt-4 flex flex-col gap-4"> <div class="ms-4 mt-4 flex flex-col gap-4">
<SettingSwitch <SettingSwitch
title={$t('admin.library_scanning_enable_description')} title={$t('admin.library_scanning_enable_description')}
{disabled} {disabled}
bind:checked={config.library.scan.enabled} bind:checked={configToEdit.library.scan.enabled}
/> />
<SettingSelect <SettingSelect
options={cronExpressionOptions} options={cronExpressionOptions}
disabled={disabled || !config.library.scan.enabled} disabled={disabled || !configToEdit.library.scan.enabled}
name="expression" name="expression"
label={$t('admin.cron_expression_presets')} label={$t('admin.cron_expression_presets')}
bind:value={config.library.scan.cronExpression} bind:value={configToEdit.library.scan.cronExpression}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
required={true} required={true}
disabled={disabled || !config.library.scan.enabled} disabled={disabled || !configToEdit.library.scan.enabled}
label={$t('admin.cron_expression')} label={$t('admin.cron_expression')}
bind:value={config.library.scan.cronExpression} bind:value={configToEdit.library.scan.cronExpression}
isEdited={config.library.scan.cronExpression !== savedConfig.library.scan.cronExpression} isEdited={configToEdit.library.scan.cronExpression !== config.library.scan.cronExpression}
> >
{#snippet descriptionSnippet()} {#snippet descriptionSnippet()}
<p class="text-sm dark:text-immich-dark-fg"> <p class="text-sm dark:text-immich-dark-fg">
<FormatMessage key="admin.cron_expression_description"> <FormatMessage key="admin.cron_expression_description">
{#snippet children({ message })} {#snippet children({ message })}
<a <a
href="https://crontab.guru/#{config.library.scan.cronExpression.replaceAll(' ', '_')}" href="https://crontab.guru/#{configToEdit.library.scan.cronExpression.replaceAll(' ', '_')}"
class="underline" class="underline"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
@ -112,12 +88,7 @@
</div> </div>
</SettingAccordion> </SettingAccordion>
<SettingButtonsRow <SettingButtonsRow bind:configToEdit keys={['library']} {disabled} />
onReset={(options) => onReset({ ...options, configKeys: ['library'] })}
onSave={() => onSave({ library: config.library })}
showResetToDefault={!isEqual(savedConfig.library, defaultConfig.library)}
{disabled}
/>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,42 +1,30 @@
<script lang="ts"> <script lang="ts">
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte'; import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte'; import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { LogLevel, type SystemConfigDto } from '@immich/sdk'; import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { isEqual } from 'lodash-es'; import { LogLevel } from '@immich/sdk';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from './admin-settings';
interface Props { const disabled = $featureFlags.configFile;
savedConfig: SystemConfigDto; const config = $derived(systemConfigManager.value);
defaultConfig: SystemConfigDto; let configToEdit = $state(systemConfigManager.cloneValue());
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> </script>
<div> <div>
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<form autocomplete="off" {onsubmit}> <form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
<div class="ms-4 mt-4 flex flex-col gap-4"> <div class="ms-4 mt-4 flex flex-col gap-4">
<SettingSwitch <SettingSwitch
title={$t('admin.logging_enable_description')} title={$t('admin.logging_enable_description')}
{disabled} {disabled}
bind:checked={config.logging.enabled} bind:checked={configToEdit.logging.enabled}
/> />
<SettingSelect <SettingSelect
label={$t('level')} label={$t('level')}
desc={$t('admin.logging_level_description')} desc={$t('admin.logging_level_description')}
bind:value={config.logging.level} bind:value={configToEdit.logging.level}
options={[ options={[
{ value: LogLevel.Fatal, text: 'Fatal' }, { value: LogLevel.Fatal, text: 'Fatal' },
{ value: LogLevel.Error, text: 'Error' }, { value: LogLevel.Error, text: 'Error' },
@ -46,16 +34,11 @@
{ value: LogLevel.Verbose, text: 'Verbose' }, { value: LogLevel.Verbose, text: 'Verbose' },
]} ]}
name="level" name="level"
isEdited={config.logging.level !== savedConfig.logging.level} isEdited={configToEdit.logging.level !== config.logging.level}
disabled={disabled || !config.logging.enabled} disabled={disabled || !configToEdit.logging.enabled}
/> />
<SettingButtonsRow <SettingButtonsRow bind:configToEdit keys={['logging']} {disabled} />
onReset={(options) => onReset({ ...options, configKeys: ['logging'] })}
onSave={() => onSave({ logging: config.logging })}
showResetToDefault={!isEqual(savedConfig.logging, defaultConfig.logging)}
{disabled}
/>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,65 +1,52 @@
<script lang="ts"> <script lang="ts">
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.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 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 SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import { SettingInputFieldType } from '$lib/constants'; import { SettingInputFieldType } from '$lib/constants';
import FormatMessage from '$lib/elements/FormatMessage.svelte'; import FormatMessage from '$lib/elements/FormatMessage.svelte';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import type { SystemConfigDto } from '@immich/sdk';
import { Button, IconButton } from '@immich/ui'; import { Button, IconButton } from '@immich/ui';
import { mdiPlus, mdiTrashCanOutline } from '@mdi/js'; import { mdiPlus, mdiTrashCanOutline } from '@mdi/js';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from './admin-settings';
interface Props { const disabled = $featureFlags.configFile;
savedConfig: SystemConfigDto; const config = $derived(systemConfigManager.value);
defaultConfig: SystemConfigDto; let configToEdit = $state(systemConfigManager.cloneValue());
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> </script>
<div class="mt-2"> <div class="mt-2">
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<form autocomplete="off" {onsubmit} class="mx-4 mt-4"> <form autocomplete="off" class="mx-4 mt-4" onsubmit={(event) => event.preventDefault()}>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<SettingSwitch <SettingSwitch
title={$t('admin.machine_learning_enabled')} title={$t('admin.machine_learning_enabled')}
subtitle={$t('admin.machine_learning_enabled_description')} subtitle={$t('admin.machine_learning_enabled_description')}
{disabled} {disabled}
bind:checked={config.machineLearning.enabled} bind:checked={configToEdit.machineLearning.enabled}
/> />
<hr /> <hr />
<div> <div>
{#each config.machineLearning.urls as _, i (i)} {#each configToEdit.machineLearning.urls as _, i (i)}
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label={i === 0 ? $t('url') : undefined} label={i === 0 ? $t('url') : undefined}
description={i === 0 ? $t('admin.machine_learning_url_description') : undefined} description={i === 0 ? $t('admin.machine_learning_url_description') : undefined}
bind:value={config.machineLearning.urls[i]} bind:value={configToEdit.machineLearning.urls[i]}
required={i === 0} required={i === 0}
disabled={disabled || !config.machineLearning.enabled} disabled={disabled || !configToEdit.machineLearning.enabled}
isEdited={i === 0 && !isEqual(config.machineLearning.urls, savedConfig.machineLearning.urls)} isEdited={i === 0 && !isEqual(configToEdit.machineLearning.urls, config.machineLearning.urls)}
> >
{#snippet trailingSnippet()} {#snippet trailingSnippet()}
{#if config.machineLearning.urls.length > 1} {#if configToEdit.machineLearning.urls.length > 1}
<IconButton <IconButton
aria-label="" aria-label=""
onclick={() => config.machineLearning.urls.splice(i, 1)} onclick={() => configToEdit.machineLearning.urls.splice(i, 1)}
icon={mdiTrashCanOutline} icon={mdiTrashCanOutline}
color="danger" color="danger"
/> />
@ -75,8 +62,8 @@
size="small" size="small"
shape="round" shape="round"
leadingIcon={mdiPlus} leadingIcon={mdiPlus}
onclick={() => config.machineLearning.urls.push('')} onclick={() => configToEdit.machineLearning.urls.push('')}
disabled={disabled || !config.machineLearning.enabled}>{$t('add_url')}</Button disabled={disabled || !configToEdit.machineLearning.enabled}>{$t('add_url')}</Button
> >
</div> </div>
</div> </div>
@ -89,8 +76,8 @@
<div class="ms-4 mt-4 flex flex-col gap-4"> <div class="ms-4 mt-4 flex flex-col gap-4">
<SettingSwitch <SettingSwitch
title={$t('admin.machine_learning_availability_checks_enabled')} title={$t('admin.machine_learning_availability_checks_enabled')}
bind:checked={config.machineLearning.availabilityChecks.enabled} bind:checked={configToEdit.machineLearning.availabilityChecks.enabled}
disabled={disabled || !config.machineLearning.enabled} disabled={disabled || !configToEdit.machineLearning.enabled}
/> />
<hr /> <hr />
@ -98,21 +85,25 @@
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_availability_checks_interval')} label={$t('admin.machine_learning_availability_checks_interval')}
bind:value={config.machineLearning.availabilityChecks.interval} bind:value={configToEdit.machineLearning.availabilityChecks.interval}
description={$t('admin.machine_learning_availability_checks_interval_description')} description={$t('admin.machine_learning_availability_checks_interval_description')}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.availabilityChecks.enabled} disabled={disabled ||
isEdited={config.machineLearning.availabilityChecks.interval !== !configToEdit.machineLearning.enabled ||
savedConfig.machineLearning.availabilityChecks.interval} !configToEdit.machineLearning.availabilityChecks.enabled}
isEdited={configToEdit.machineLearning.availabilityChecks.interval !==
config.machineLearning.availabilityChecks.interval}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_availability_checks_timeout')} label={$t('admin.machine_learning_availability_checks_timeout')}
bind:value={config.machineLearning.availabilityChecks.timeout} bind:value={configToEdit.machineLearning.availabilityChecks.timeout}
description={$t('admin.machine_learning_availability_checks_timeout_description')} description={$t('admin.machine_learning_availability_checks_timeout_description')}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.availabilityChecks.enabled} disabled={disabled ||
isEdited={config.machineLearning.availabilityChecks.timeout !== !configToEdit.machineLearning.enabled ||
savedConfig.machineLearning.availabilityChecks.timeout} !configToEdit.machineLearning.availabilityChecks.enabled}
isEdited={configToEdit.machineLearning.availabilityChecks.timeout !==
config.machineLearning.availabilityChecks.timeout}
/> />
</div> </div>
</SettingAccordion> </SettingAccordion>
@ -126,8 +117,8 @@
<SettingSwitch <SettingSwitch
title={$t('admin.machine_learning_smart_search_enabled')} title={$t('admin.machine_learning_smart_search_enabled')}
subtitle={$t('admin.machine_learning_smart_search_enabled_description')} subtitle={$t('admin.machine_learning_smart_search_enabled_description')}
bind:checked={config.machineLearning.clip.enabled} bind:checked={configToEdit.machineLearning.clip.enabled}
disabled={disabled || !config.machineLearning.enabled} disabled={disabled || !configToEdit.machineLearning.enabled}
/> />
<hr /> <hr />
@ -135,10 +126,10 @@
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label={$t('admin.machine_learning_clip_model')} label={$t('admin.machine_learning_clip_model')}
bind:value={config.machineLearning.clip.modelName} bind:value={configToEdit.machineLearning.clip.modelName}
required={true} required={true}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.clip.enabled} disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.clip.enabled}
isEdited={config.machineLearning.clip.modelName !== savedConfig.machineLearning.clip.modelName} isEdited={configToEdit.machineLearning.clip.modelName !== config.machineLearning.clip.modelName}
> >
{#snippet descriptionSnippet()} {#snippet descriptionSnippet()}
<p class="immich-form-label pb-2 text-sm"> <p class="immich-form-label pb-2 text-sm">
@ -162,8 +153,8 @@
<SettingSwitch <SettingSwitch
title={$t('admin.machine_learning_duplicate_detection_enabled')} title={$t('admin.machine_learning_duplicate_detection_enabled')}
subtitle={$t('admin.machine_learning_duplicate_detection_enabled_description')} subtitle={$t('admin.machine_learning_duplicate_detection_enabled_description')}
bind:checked={config.machineLearning.duplicateDetection.enabled} bind:checked={configToEdit.machineLearning.duplicateDetection.enabled}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.clip.enabled} disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.clip.enabled}
/> />
<hr /> <hr />
@ -171,14 +162,14 @@
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_max_detection_distance')} label={$t('admin.machine_learning_max_detection_distance')}
bind:value={config.machineLearning.duplicateDetection.maxDistance} bind:value={configToEdit.machineLearning.duplicateDetection.maxDistance}
step="0.0005" step="0.0005"
min={0.001} min={0.001}
max={0.1} max={0.1}
description={$t('admin.machine_learning_max_detection_distance_description')} description={$t('admin.machine_learning_max_detection_distance_description')}
disabled={disabled || !$featureFlags.duplicateDetection} disabled={disabled || !$featureFlags.duplicateDetection}
isEdited={config.machineLearning.duplicateDetection.maxDistance !== isEdited={configToEdit.machineLearning.duplicateDetection.maxDistance !==
savedConfig.machineLearning.duplicateDetection.maxDistance} config.machineLearning.duplicateDetection.maxDistance}
/> />
</div> </div>
</SettingAccordion> </SettingAccordion>
@ -192,8 +183,8 @@
<SettingSwitch <SettingSwitch
title={$t('admin.machine_learning_facial_recognition_setting')} title={$t('admin.machine_learning_facial_recognition_setting')}
subtitle={$t('admin.machine_learning_facial_recognition_setting_description')} subtitle={$t('admin.machine_learning_facial_recognition_setting_description')}
bind:checked={config.machineLearning.facialRecognition.enabled} bind:checked={configToEdit.machineLearning.facialRecognition.enabled}
disabled={disabled || !config.machineLearning.enabled} disabled={disabled || !configToEdit.machineLearning.enabled}
/> />
<hr /> <hr />
@ -202,54 +193,62 @@
label={$t('admin.machine_learning_facial_recognition_model')} label={$t('admin.machine_learning_facial_recognition_model')}
desc={$t('admin.machine_learning_facial_recognition_model_description')} desc={$t('admin.machine_learning_facial_recognition_model_description')}
name="facial-recognition-model" name="facial-recognition-model"
bind:value={config.machineLearning.facialRecognition.modelName} bind:value={configToEdit.machineLearning.facialRecognition.modelName}
options={[ options={[
{ value: 'antelopev2', text: 'antelopev2' }, { value: 'antelopev2', text: 'antelopev2' },
{ value: 'buffalo_l', text: 'buffalo_l' }, { value: 'buffalo_l', text: 'buffalo_l' },
{ value: 'buffalo_m', text: 'buffalo_m' }, { value: 'buffalo_m', text: 'buffalo_m' },
{ value: 'buffalo_s', text: 'buffalo_s' }, { value: 'buffalo_s', text: 'buffalo_s' },
]} ]}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled} disabled={disabled ||
isEdited={config.machineLearning.facialRecognition.modelName !== !configToEdit.machineLearning.enabled ||
savedConfig.machineLearning.facialRecognition.modelName} !configToEdit.machineLearning.facialRecognition.enabled}
isEdited={configToEdit.machineLearning.facialRecognition.modelName !==
config.machineLearning.facialRecognition.modelName}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_min_detection_score')} label={$t('admin.machine_learning_min_detection_score')}
description={$t('admin.machine_learning_min_detection_score_description')} description={$t('admin.machine_learning_min_detection_score_description')}
bind:value={config.machineLearning.facialRecognition.minScore} bind:value={configToEdit.machineLearning.facialRecognition.minScore}
step="0.01" step="0.01"
min={0.1} min={0.1}
max={1} max={1}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled} disabled={disabled ||
isEdited={config.machineLearning.facialRecognition.minScore !== !configToEdit.machineLearning.enabled ||
savedConfig.machineLearning.facialRecognition.minScore} !configToEdit.machineLearning.facialRecognition.enabled}
isEdited={configToEdit.machineLearning.facialRecognition.minScore !==
config.machineLearning.facialRecognition.minScore}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_max_recognition_distance')} label={$t('admin.machine_learning_max_recognition_distance')}
description={$t('admin.machine_learning_max_recognition_distance_description')} description={$t('admin.machine_learning_max_recognition_distance_description')}
bind:value={config.machineLearning.facialRecognition.maxDistance} bind:value={configToEdit.machineLearning.facialRecognition.maxDistance}
step="0.01" step="0.01"
min={0.1} min={0.1}
max={2} max={2}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled} disabled={disabled ||
isEdited={config.machineLearning.facialRecognition.maxDistance !== !configToEdit.machineLearning.enabled ||
savedConfig.machineLearning.facialRecognition.maxDistance} !configToEdit.machineLearning.facialRecognition.enabled}
isEdited={configToEdit.machineLearning.facialRecognition.maxDistance !==
config.machineLearning.facialRecognition.maxDistance}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_min_recognized_faces')} label={$t('admin.machine_learning_min_recognized_faces')}
description={$t('admin.machine_learning_min_recognized_faces_description')} description={$t('admin.machine_learning_min_recognized_faces_description')}
bind:value={config.machineLearning.facialRecognition.minFaces} bind:value={configToEdit.machineLearning.facialRecognition.minFaces}
step="1" step="1"
min={1} min={1}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled} disabled={disabled ||
isEdited={config.machineLearning.facialRecognition.minFaces !== !configToEdit.machineLearning.enabled ||
savedConfig.machineLearning.facialRecognition.minFaces} !configToEdit.machineLearning.facialRecognition.enabled}
isEdited={configToEdit.machineLearning.facialRecognition.minFaces !==
config.machineLearning.facialRecognition.minFaces}
/> />
</div> </div>
</SettingAccordion> </SettingAccordion>
@ -263,8 +262,8 @@
<SettingSwitch <SettingSwitch
title={$t('admin.machine_learning_ocr_enabled')} title={$t('admin.machine_learning_ocr_enabled')}
subtitle={$t('admin.machine_learning_ocr_enabled_description')} subtitle={$t('admin.machine_learning_ocr_enabled_description')}
bind:checked={config.machineLearning.ocr.enabled} bind:checked={configToEdit.machineLearning.ocr.enabled}
disabled={disabled || !config.machineLearning.enabled} disabled={disabled || !configToEdit.machineLearning.enabled}
/> />
<hr /> <hr />
@ -273,7 +272,7 @@
label={$t('admin.machine_learning_ocr_model')} label={$t('admin.machine_learning_ocr_model')}
desc={$t('admin.machine_learning_ocr_model_description')} desc={$t('admin.machine_learning_ocr_model_description')}
name="ocr-model" name="ocr-model"
bind:value={config.machineLearning.ocr.modelName} bind:value={configToEdit.machineLearning.ocr.modelName}
options={[ options={[
{ text: 'PP-OCRv5_server (Chinese, Japanese and English)', value: 'PP-OCRv5_server' }, { text: 'PP-OCRv5_server (Chinese, Japanese and English)', value: 'PP-OCRv5_server' },
{ text: 'PP-OCRv5_mobile (Chinese, Japanese and English)', value: 'PP-OCRv5_mobile' }, { text: 'PP-OCRv5_mobile (Chinese, Japanese and English)', value: 'PP-OCRv5_mobile' },
@ -284,53 +283,48 @@
{ text: 'PP-OCRv5_mobile (Russian, Belarusian, Ukrainian and English)', value: 'ESLAV__PP-OCRv5_mobile' }, { text: 'PP-OCRv5_mobile (Russian, Belarusian, Ukrainian and English)', value: 'ESLAV__PP-OCRv5_mobile' },
{ text: 'PP-OCRv5_mobile (Thai and English)', value: 'TH__PP-OCRv5_mobile' }, { text: 'PP-OCRv5_mobile (Thai and English)', value: 'TH__PP-OCRv5_mobile' },
]} ]}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.ocr.enabled} disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.ocr.enabled}
isEdited={config.machineLearning.ocr.modelName !== savedConfig.machineLearning.ocr.modelName} isEdited={configToEdit.machineLearning.ocr.modelName !== config.machineLearning.ocr.modelName}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_ocr_min_detection_score')} label={$t('admin.machine_learning_ocr_min_detection_score')}
description={$t('admin.machine_learning_ocr_min_detection_score_description')} description={$t('admin.machine_learning_ocr_min_detection_score_description')}
bind:value={config.machineLearning.ocr.minDetectionScore} bind:value={configToEdit.machineLearning.ocr.minDetectionScore}
step="0.1" step="0.1"
min={0.1} min={0.1}
max={1} max={1}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.ocr.enabled} disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.ocr.enabled}
isEdited={config.machineLearning.ocr.minDetectionScore !== isEdited={configToEdit.machineLearning.ocr.minDetectionScore !==
savedConfig.machineLearning.ocr.minDetectionScore} config.machineLearning.ocr.minDetectionScore}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_ocr_min_recognition_score')} label={$t('admin.machine_learning_ocr_min_recognition_score')}
description={$t('admin.machine_learning_ocr_min_score_recognition_description')} description={$t('admin.machine_learning_ocr_min_score_recognition_description')}
bind:value={config.machineLearning.ocr.minRecognitionScore} bind:value={configToEdit.machineLearning.ocr.minRecognitionScore}
step="0.1" step="0.1"
min={0.1} min={0.1}
max={1} max={1}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.ocr.enabled} disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.ocr.enabled}
isEdited={config.machineLearning.ocr.minRecognitionScore !== isEdited={configToEdit.machineLearning.ocr.minRecognitionScore !==
savedConfig.machineLearning.ocr.minRecognitionScore} config.machineLearning.ocr.minRecognitionScore}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_ocr_max_resolution')} label={$t('admin.machine_learning_ocr_max_resolution')}
description={$t('admin.machine_learning_ocr_max_resolution_description')} description={$t('admin.machine_learning_ocr_max_resolution_description')}
bind:value={config.machineLearning.ocr.maxResolution} bind:value={configToEdit.machineLearning.ocr.maxResolution}
min={1} min={1}
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.ocr.enabled} disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.ocr.enabled}
isEdited={config.machineLearning.ocr.maxResolution !== savedConfig.machineLearning.ocr.maxResolution} isEdited={configToEdit.machineLearning.ocr.maxResolution !== config.machineLearning.ocr.maxResolution}
/> />
</div> </div>
</SettingAccordion> </SettingAccordion>
<SettingButtonsRow <SettingButtonsRow bind:configToEdit keys={['machineLearning']} {disabled} />
onReset={(options) => onReset({ ...options, configKeys: ['machineLearning'] })}
onSave={() => onSave({ machineLearning: config.machineLearning })}
showResetToDefault={!isEqual(savedConfig.machineLearning, defaultConfig.machineLearning)}
{disabled}
/>
</form> </form>
</div> </div>
</div> </div>

View File

@ -1,35 +1,22 @@
<script lang="ts"> <script lang="ts">
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.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 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 SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import { SettingInputFieldType } from '$lib/constants'; import { SettingInputFieldType } from '$lib/constants';
import FormatMessage from '$lib/elements/FormatMessage.svelte'; import FormatMessage from '$lib/elements/FormatMessage.svelte';
import type { SystemConfigDto } from '@immich/sdk'; import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { isEqual } from 'lodash-es';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from './admin-settings';
interface Props { const disabled = $featureFlags.configFile;
savedConfig: SystemConfigDto; const config = $derived(systemConfigManager.value);
defaultConfig: SystemConfigDto; let configToEdit = $state(systemConfigManager.cloneValue());
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> </script>
<div class="mt-2"> <div class="mt-2">
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<form autocomplete="off" {onsubmit}> <form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<SettingAccordion key="map" title={$t('admin.map_settings')} subtitle={$t('admin.map_settings_description')}> <SettingAccordion key="map" title={$t('admin.map_settings')} subtitle={$t('admin.map_settings_description')}>
<div class="ms-4 mt-4 flex flex-col gap-4"> <div class="ms-4 mt-4 flex flex-col gap-4">
@ -37,7 +24,7 @@
title={$t('admin.map_enable_description')} title={$t('admin.map_enable_description')}
subtitle={$t('admin.map_implications')} subtitle={$t('admin.map_implications')}
{disabled} {disabled}
bind:checked={config.map.enabled} bind:checked={configToEdit.map.enabled}
/> />
<hr /> <hr />
@ -46,17 +33,17 @@
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label={$t('admin.map_light_style')} label={$t('admin.map_light_style')}
description={$t('admin.map_style_description')} description={$t('admin.map_style_description')}
bind:value={config.map.lightStyle} bind:value={configToEdit.map.lightStyle}
disabled={disabled || !config.map.enabled} disabled={disabled || !configToEdit.map.enabled}
isEdited={config.map.lightStyle !== savedConfig.map.lightStyle} isEdited={configToEdit.map.lightStyle !== config.map.lightStyle}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label={$t('admin.map_dark_style')} label={$t('admin.map_dark_style')}
description={$t('admin.map_style_description')} description={$t('admin.map_style_description')}
bind:value={config.map.darkStyle} bind:value={configToEdit.map.darkStyle}
disabled={disabled || !config.map.enabled} disabled={disabled || !configToEdit.map.enabled}
isEdited={config.map.darkStyle !== savedConfig.map.darkStyle} isEdited={configToEdit.map.darkStyle !== config.map.darkStyle}
/> />
</div></SettingAccordion </div></SettingAccordion
> >
@ -82,20 +69,12 @@
<SettingSwitch <SettingSwitch
title={$t('admin.map_reverse_geocoding_enable_description')} title={$t('admin.map_reverse_geocoding_enable_description')}
{disabled} {disabled}
bind:checked={config.reverseGeocoding.enabled} bind:checked={configToEdit.reverseGeocoding.enabled}
/> />
</div></SettingAccordion </div></SettingAccordion
> >
<SettingButtonsRow <SettingButtonsRow bind:configToEdit keys={['map', 'reverseGeocoding']} {disabled} />
onReset={(options) => onReset({ ...options, configKeys: ['map', 'reverseGeocoding'] })}
onSave={() => onSave({ map: config.map, reverseGeocoding: config.reverseGeocoding })}
showResetToDefault={!isEqual(
{ map: savedConfig.map, reverseGeocoding: savedConfig.reverseGeocoding },
{ map: defaultConfig.map, reverseGeocoding: defaultConfig.reverseGeocoding },
)}
{disabled}
/>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,46 +1,27 @@
<script lang="ts"> <script lang="ts">
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte'; import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import type { SystemConfigDto } from '@immich/sdk'; import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { isEqual } from 'lodash-es';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from './admin-settings';
interface Props { const disabled = $featureFlags.configFile;
savedConfig: SystemConfigDto; let configToEdit = $state(systemConfigManager.cloneValue());
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> </script>
<div class="mt-2"> <div class="mt-2">
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<form autocomplete="off" {onsubmit} class="mx-4 mt-4"> <form autocomplete="off" class="mx-4 mt-4" onsubmit={(event) => event.preventDefault()}>
<div class="ms-4 mt-4 flex flex-col gap-4"> <div class="ms-4 mt-4 flex flex-col gap-4">
<SettingSwitch <SettingSwitch
title={$t('admin.metadata_faces_import_setting')} title={$t('admin.metadata_faces_import_setting')}
subtitle={$t('admin.metadata_faces_import_setting_description')} subtitle={$t('admin.metadata_faces_import_setting_description')}
bind:checked={config.metadata.faces.import} bind:checked={configToEdit.metadata.faces.import}
{disabled} {disabled}
/> />
</div> </div>
<SettingButtonsRow <SettingButtonsRow bind:configToEdit keys={['metadata']} {disabled} />
onReset={(options) => onReset({ ...options, configKeys: ['metadata'] })}
onSave={() => onSave({ metadata: config.metadata })}
showResetToDefault={!isEqual(savedConfig.metadata.faces.import, defaultConfig.metadata.faces.import)}
{disabled}
/>
</form> </form>
</div> </div>
</div> </div>

View File

@ -1,44 +1,25 @@
<script lang="ts"> <script lang="ts">
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte'; import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import type { SystemConfigDto } from '@immich/sdk'; import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { isEqual } from 'lodash-es';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from './admin-settings';
interface Props { const disabled = $featureFlags.configFile;
savedConfig: SystemConfigDto; let configToEdit = $state(systemConfigManager.cloneValue());
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> </script>
<div> <div>
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<form autocomplete="off" {onsubmit}> <form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
<div class="ms-4 mt-4"> <div class="ms-4 mt-4">
<SettingSwitch <SettingSwitch
title={$t('admin.version_check_enabled_description')} title={$t('admin.version_check_enabled_description')}
subtitle={$t('admin.version_check_implications')} subtitle={$t('admin.version_check_implications')}
bind:checked={config.newVersionCheck.enabled} bind:checked={configToEdit.newVersionCheck.enabled}
{disabled}
/>
<SettingButtonsRow
onReset={(options) => onReset({ ...options, configKeys: ['newVersionCheck'] })}
onSave={() => onSave({ newVersionCheck: config.newVersionCheck })}
showResetToDefault={!isEqual(savedConfig.newVersionCheck, defaultConfig.newVersionCheck)}
{disabled} {disabled}
/> />
<SettingButtonsRow bind:configToEdit keys={['newVersionCheck']} {disabled} />
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,81 +1,63 @@
<script lang="ts"> <script lang="ts">
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte'; import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingInputField 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 SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { SettingInputFieldType } from '$lib/constants'; import { SettingInputFieldType } from '$lib/constants';
import type { SystemConfigDto } from '@immich/sdk'; import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { isEqual } from 'lodash-es';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from './admin-settings';
interface Props { const disabled = $featureFlags.configFile;
savedConfig: SystemConfigDto; const config = $derived(systemConfigManager.value);
defaultConfig: SystemConfigDto; let configToEdit = $state(systemConfigManager.cloneValue());
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> </script>
<div class="mt-2"> <div class="mt-2">
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<form autocomplete="off" {onsubmit} class="mx-4 mt-4"> <form autocomplete="off" class="mx-4 mt-4" onsubmit={(event) => event.preventDefault()}>
<div class="ms-4 mt-4 flex flex-col gap-4"> <div class="ms-4 mt-4 flex flex-col gap-4">
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label={$t('admin.nightly_tasks_start_time_setting')} label={$t('admin.nightly_tasks_start_time_setting')}
description={$t('admin.nightly_tasks_start_time_setting_description')} description={$t('admin.nightly_tasks_start_time_setting_description')}
bind:value={config.nightlyTasks.startTime} bind:value={configToEdit.nightlyTasks.startTime}
required={true} required={true}
{disabled} {disabled}
isEdited={!(config.nightlyTasks.startTime === savedConfig.nightlyTasks.startTime)} isEdited={!(configToEdit.nightlyTasks.startTime === config.nightlyTasks.startTime)}
/> />
<SettingSwitch <SettingSwitch
title={$t('admin.nightly_tasks_database_cleanup_setting')} title={$t('admin.nightly_tasks_database_cleanup_setting')}
subtitle={$t('admin.nightly_tasks_database_cleanup_setting_description')} subtitle={$t('admin.nightly_tasks_database_cleanup_setting_description')}
bind:checked={config.nightlyTasks.databaseCleanup} bind:checked={configToEdit.nightlyTasks.databaseCleanup}
{disabled} {disabled}
/> />
<SettingSwitch <SettingSwitch
title={$t('admin.nightly_tasks_missing_thumbnails_setting')} title={$t('admin.nightly_tasks_missing_thumbnails_setting')}
subtitle={$t('admin.nightly_tasks_missing_thumbnails_setting_description')} subtitle={$t('admin.nightly_tasks_missing_thumbnails_setting_description')}
bind:checked={config.nightlyTasks.missingThumbnails} bind:checked={configToEdit.nightlyTasks.missingThumbnails}
{disabled} {disabled}
/> />
<SettingSwitch <SettingSwitch
title={$t('admin.nightly_tasks_cluster_new_faces_setting')} title={$t('admin.nightly_tasks_cluster_new_faces_setting')}
subtitle={$t('admin.nightly_tasks_cluster_faces_setting_description')} subtitle={$t('admin.nightly_tasks_cluster_faces_setting_description')}
bind:checked={config.nightlyTasks.clusterNewFaces} bind:checked={configToEdit.nightlyTasks.clusterNewFaces}
{disabled} {disabled}
/> />
<SettingSwitch <SettingSwitch
title={$t('admin.nightly_tasks_generate_memories_setting')} title={$t('admin.nightly_tasks_generate_memories_setting')}
subtitle={$t('admin.nightly_tasks_generate_memories_setting_description')} subtitle={$t('admin.nightly_tasks_generate_memories_setting_description')}
bind:checked={config.nightlyTasks.generateMemories} bind:checked={configToEdit.nightlyTasks.generateMemories}
{disabled} {disabled}
/> />
<SettingSwitch <SettingSwitch
title={$t('admin.nightly_tasks_sync_quota_usage_setting')} title={$t('admin.nightly_tasks_sync_quota_usage_setting')}
subtitle={$t('admin.nightly_tasks_sync_quota_usage_setting_description')} subtitle={$t('admin.nightly_tasks_sync_quota_usage_setting_description')}
bind:checked={config.nightlyTasks.syncQuotaUsage} bind:checked={configToEdit.nightlyTasks.syncQuotaUsage}
{disabled} {disabled}
/> />
</div> </div>
<SettingButtonsRow <SettingButtonsRow bind:configToEdit keys={['nightlyTasks']} {disabled} />
onReset={(options) => onReset({ ...options, configKeys: ['nightlyTasks'] })}
onSave={() => onSave({ nightlyTasks: config.nightlyTasks })}
showResetToDefault={!isEqual(savedConfig.nightlyTasks, defaultConfig.nightlyTasks)}
{disabled}
/>
</form> </form>
</div> </div>
</div> </div>

View File

@ -1,29 +1,22 @@
<script lang="ts"> <script lang="ts">
import TemplateSettings from '$lib/components/admin-settings/TemplateSettings.svelte'; import TemplateSettings from '$lib/components/admin-settings/TemplateSettings.svelte';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.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 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 SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import { SettingInputFieldType } from '$lib/constants'; import { SettingInputFieldType } from '$lib/constants';
import { handleSystemConfigSave } from '$lib/services/system-config.service';
import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { sendTestEmailAdmin, type SystemConfigDto } from '@immich/sdk'; import { sendTestEmailAdmin } from '@immich/sdk';
import { Button, LoadingSpinner, toastManager } from '@immich/ui'; import { Button, LoadingSpinner, toastManager } from '@immich/ui';
import { isEqual } from 'lodash-es';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from './admin-settings';
interface Props { const disabled = $featureFlags.configFile;
savedConfig: SystemConfigDto; const config = $derived(systemConfigManager.value);
defaultConfig: SystemConfigDto; let configToEdit = $state(systemConfigManager.cloneValue());
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
let isSending = $state(false); let isSending = $state(false);
@ -37,24 +30,24 @@
try { try {
await sendTestEmailAdmin({ await sendTestEmailAdmin({
systemConfigSmtpDto: { systemConfigSmtpDto: {
enabled: config.notifications.smtp.enabled, enabled: configToEdit.notifications.smtp.enabled,
transport: { transport: {
host: config.notifications.smtp.transport.host, host: configToEdit.notifications.smtp.transport.host,
port: config.notifications.smtp.transport.port, port: configToEdit.notifications.smtp.transport.port,
secure: config.notifications.smtp.transport.secure, secure: configToEdit.notifications.smtp.transport.secure,
username: config.notifications.smtp.transport.username, username: configToEdit.notifications.smtp.transport.username,
password: config.notifications.smtp.transport.password, password: configToEdit.notifications.smtp.transport.password,
ignoreCert: config.notifications.smtp.transport.ignoreCert, ignoreCert: configToEdit.notifications.smtp.transport.ignoreCert,
}, },
from: config.notifications.smtp.from, from: configToEdit.notifications.smtp.from,
replyTo: config.notifications.smtp.from, replyTo: configToEdit.notifications.smtp.from,
}, },
}); });
toastManager.success($t('admin.notification_email_test_email_sent', { values: { email: $user.email } })); toastManager.success($t('admin.notification_email_test_email_sent', { values: { email: $user.email } }));
if (!disabled) { if (!disabled) {
onSave({ notifications: config.notifications }); await handleSystemConfigSave({ notifications: configToEdit.notifications });
} }
} catch (error) { } catch (error) {
handleError(error, $t('admin.notification_email_test_email_failed')); handleError(error, $t('admin.notification_email_test_email_failed'));
@ -62,22 +55,18 @@
isSending = false; isSending = false;
} }
}; };
const onsubmit = (event: Event) => {
event.preventDefault();
};
</script> </script>
<div> <div>
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<form autocomplete="off" {onsubmit} class="mt-4"> <form autocomplete="off" class="mt-4" onsubmit={(event) => event.preventDefault()}>
<div class="flex flex-col gap-4"> <div class="flex flex-col gap-4">
<SettingAccordion key="email" title={$t('email')} subtitle={$t('admin.notification_email_setting_description')}> <SettingAccordion key="email" title={$t('email')} subtitle={$t('admin.notification_email_setting_description')}>
<div class="ms-4 mt-4 flex flex-col gap-4"> <div class="ms-4 mt-4 flex flex-col gap-4">
<SettingSwitch <SettingSwitch
title={$t('admin.notification_enable_email_notifications')} title={$t('admin.notification_enable_email_notifications')}
{disabled} {disabled}
bind:checked={config.notifications.smtp.enabled} bind:checked={configToEdit.notifications.smtp.enabled}
/> />
<hr /> <hr />
@ -87,9 +76,9 @@
required required
label={$t('host')} label={$t('host')}
description={$t('admin.notification_email_host_description')} description={$t('admin.notification_email_host_description')}
disabled={disabled || !config.notifications.smtp.enabled} disabled={disabled || !configToEdit.notifications.smtp.enabled}
bind:value={config.notifications.smtp.transport.host} bind:value={configToEdit.notifications.smtp.transport.host}
isEdited={config.notifications.smtp.transport.host !== savedConfig.notifications.smtp.transport.host} isEdited={configToEdit.notifications.smtp.transport.host !== config.notifications.smtp.transport.host}
/> />
<SettingInputField <SettingInputField
@ -97,43 +86,43 @@
required required
label={$t('port')} label={$t('port')}
description={$t('admin.notification_email_port_description')} description={$t('admin.notification_email_port_description')}
disabled={disabled || !config.notifications.smtp.enabled} disabled={disabled || !configToEdit.notifications.smtp.enabled}
bind:value={config.notifications.smtp.transport.port} bind:value={configToEdit.notifications.smtp.transport.port}
isEdited={config.notifications.smtp.transport.port !== savedConfig.notifications.smtp.transport.port} isEdited={configToEdit.notifications.smtp.transport.port !== config.notifications.smtp.transport.port}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label={$t('username')} label={$t('username')}
description={$t('admin.notification_email_username_description')} description={$t('admin.notification_email_username_description')}
disabled={disabled || !config.notifications.smtp.enabled} disabled={disabled || !configToEdit.notifications.smtp.enabled}
bind:value={config.notifications.smtp.transport.username} bind:value={configToEdit.notifications.smtp.transport.username}
isEdited={config.notifications.smtp.transport.username !== isEdited={configToEdit.notifications.smtp.transport.username !==
savedConfig.notifications.smtp.transport.username} config.notifications.smtp.transport.username}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.PASSWORD} inputType={SettingInputFieldType.PASSWORD}
label={$t('password')} label={$t('password')}
description={$t('admin.notification_email_password_description')} description={$t('admin.notification_email_password_description')}
disabled={disabled || !config.notifications.smtp.enabled} disabled={disabled || !configToEdit.notifications.smtp.enabled}
bind:value={config.notifications.smtp.transport.password} bind:value={configToEdit.notifications.smtp.transport.password}
isEdited={config.notifications.smtp.transport.password !== isEdited={configToEdit.notifications.smtp.transport.password !==
savedConfig.notifications.smtp.transport.password} config.notifications.smtp.transport.password}
/> />
<SettingSwitch <SettingSwitch
title={$t('admin.notification_email_secure')} title={$t('admin.notification_email_secure')}
subtitle={$t('admin.notification_email_secure_description')} subtitle={$t('admin.notification_email_secure_description')}
disabled={disabled || !config.notifications.smtp.enabled} disabled={disabled || !configToEdit.notifications.smtp.enabled}
bind:checked={config.notifications.smtp.transport.secure} bind:checked={configToEdit.notifications.smtp.transport.secure}
/> />
<SettingSwitch <SettingSwitch
title={$t('admin.notification_email_ignore_certificate_errors')} title={$t('admin.notification_email_ignore_certificate_errors')}
subtitle={$t('admin.notification_email_ignore_certificate_errors_description')} subtitle={$t('admin.notification_email_ignore_certificate_errors_description')}
disabled={disabled || !config.notifications.smtp.enabled} disabled={disabled || !configToEdit.notifications.smtp.enabled}
bind:checked={config.notifications.smtp.transport.ignoreCert} bind:checked={configToEdit.notifications.smtp.transport.ignoreCert}
/> />
<hr /> <hr />
@ -143,16 +132,16 @@
required required
label={$t('admin.notification_email_from_address')} label={$t('admin.notification_email_from_address')}
description={$t('admin.notification_email_from_address_description')} description={$t('admin.notification_email_from_address_description')}
disabled={disabled || !config.notifications.smtp.enabled} disabled={disabled || !configToEdit.notifications.smtp.enabled}
bind:value={config.notifications.smtp.from} bind:value={configToEdit.notifications.smtp.from}
isEdited={config.notifications.smtp.from !== savedConfig.notifications.smtp.from} isEdited={configToEdit.notifications.smtp.from !== config.notifications.smtp.from}
/> />
<div class="flex gap-2 place-items-center"> <div class="flex gap-2 place-items-center">
<Button <Button
size="small" size="small"
shape="round" shape="round"
disabled={!config.notifications.smtp.enabled} disabled={!configToEdit.notifications.smtp.enabled}
onclick={handleSendTestEmail} onclick={handleSendTestEmail}
> >
{#if disabled} {#if disabled}
@ -170,12 +159,7 @@
</div> </div>
</form> </form>
</div> </div>
<TemplateSettings {config} {savedConfig} /> <TemplateSettings bind:config={configToEdit} />
<SettingButtonsRow <SettingButtonsRow bind:configToEdit keys={['notifications', 'templates']} {disabled} />
onReset={(options) => onReset({ ...options, configKeys: ['notifications', 'templates'] })}
onSave={() => onSave({ notifications: config.notifications, templates: config.templates })}
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
{disabled}
/>
</div> </div>

View File

@ -1,64 +1,46 @@
<script lang="ts"> <script lang="ts">
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte'; import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingInputField 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 SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { SettingInputFieldType } from '$lib/constants'; import { SettingInputFieldType } from '$lib/constants';
import type { SystemConfigDto } from '@immich/sdk'; import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { isEqual } from 'lodash-es';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from './admin-settings';
interface Props { const disabled = $featureFlags.configFile;
savedConfig: SystemConfigDto; const config = $derived(systemConfigManager.value);
defaultConfig: SystemConfigDto; let configToEdit = $state(systemConfigManager.cloneValue());
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> </script>
<div> <div>
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<form autocomplete="off" {onsubmit}> <form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
<div class="mt-4 ms-4"> <div class="mt-4 ms-4">
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label={$t('admin.server_external_domain_settings')} label={$t('admin.server_external_domain_settings')}
description={$t('admin.server_external_domain_settings_description')} description={$t('admin.server_external_domain_settings_description')}
bind:value={config.server.externalDomain} bind:value={configToEdit.server.externalDomain}
isEdited={config.server.externalDomain !== savedConfig.server.externalDomain} isEdited={configToEdit.server.externalDomain !== config.server.externalDomain}
/> />
<SettingInputField <SettingInputField
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
label={$t('admin.server_welcome_message')} label={$t('admin.server_welcome_message')}
description={$t('admin.server_welcome_message_description')} description={$t('admin.server_welcome_message_description')}
bind:value={config.server.loginPageMessage} bind:value={configToEdit.server.loginPageMessage}
isEdited={config.server.loginPageMessage !== savedConfig.server.loginPageMessage} isEdited={configToEdit.server.loginPageMessage !== config.server.loginPageMessage}
/> />
<SettingSwitch <SettingSwitch
title={$t('admin.server_public_users')} title={$t('admin.server_public_users')}
subtitle={$t('admin.server_public_users_description')} subtitle={$t('admin.server_public_users_description')}
{disabled} {disabled}
bind:checked={config.server.publicUsers} bind:checked={configToEdit.server.publicUsers}
/> />
<div class="ms-4"> <div class="ms-4">
<SettingButtonsRow <SettingButtonsRow bind:configToEdit keys={['server']} {disabled} />
onReset={(options) => onReset({ ...options, configKeys: ['server'] })}
onSave={() => onSave({ server: config.server })}
showResetToDefault={!isEqual(savedConfig.server, defaultConfig.server)}
{disabled}
/>
</div> </div>
</div> </div>
</form> </form>

View File

@ -2,50 +2,34 @@
import { resolve } from '$app/paths'; import { resolve } from '$app/paths';
import SupportedDatetimePanel from '$lib/components/admin-settings/SupportedDatetimePanel.svelte'; import SupportedDatetimePanel from '$lib/components/admin-settings/SupportedDatetimePanel.svelte';
import SupportedVariablesPanel from '$lib/components/admin-settings/SupportedVariablesPanel.svelte'; import SupportedVariablesPanel from '$lib/components/admin-settings/SupportedVariablesPanel.svelte';
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte'; import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingInputField 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 SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { AppRoute, SettingInputFieldType } from '$lib/constants'; import { AppRoute, SettingInputFieldType } from '$lib/constants';
import FormatMessage from '$lib/elements/FormatMessage.svelte'; import FormatMessage from '$lib/elements/FormatMessage.svelte';
import { handleSystemConfigSave } from '$lib/services/system-config.service';
import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { import { getStorageTemplateOptions, type SystemConfigTemplateStorageOptionDto } from '@immich/sdk';
getStorageTemplateOptions,
type SystemConfigDto,
type SystemConfigTemplateStorageOptionDto,
} from '@immich/sdk';
import { LoadingSpinner } from '@immich/ui'; import { LoadingSpinner } from '@immich/ui';
import handlebar from 'handlebars'; import handlebar from 'handlebars';
import { isEqual } from 'lodash-es';
import * as luxon from 'luxon'; import * as luxon from 'luxon';
import type { Snippet } from 'svelte'; import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { createBubbler, preventDefault } from 'svelte/legacy'; import { createBubbler, preventDefault } from 'svelte/legacy';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from './admin-settings';
interface Props { type Props = {
savedConfig: SystemConfigDto;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
disabled?: boolean;
minified?: boolean; minified?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
duration?: number; duration?: number;
children?: Snippet; saveOnClose?: boolean;
} };
let { const { minified = false, duration = 500, saveOnClose = false }: Props = $props();
savedConfig,
defaultConfig, const disabled = $featureFlags.configFile;
config = $bindable(), const config = $derived(systemConfigManager.value);
disabled = false, let configToEdit = $state(systemConfigManager.cloneValue());
minified = false,
onReset,
onSave,
duration = 500,
children,
}: Props = $props();
const bubble = createBubbler(); const bubble = createBubbler();
let templateOptions: SystemConfigTemplateStorageOptionDto | undefined = $state(); let templateOptions: SystemConfigTemplateStorageOptionDto | undefined = $state();
@ -53,7 +37,7 @@
const getTemplateOptions = async () => { const getTemplateOptions = async () => {
templateOptions = await getStorageTemplateOptions(); templateOptions = await getStorageTemplateOptions();
selectedPreset = savedConfig.storageTemplate.template; selectedPreset = config.storageTemplate.template;
}; };
const getSupportDateTimeFormat = () => getStorageTemplateOptions(); const getSupportDateTimeFormat = () => getStorageTemplateOptions();
@ -101,15 +85,21 @@
}; };
const handlePresetSelection = () => { const handlePresetSelection = () => {
config.storageTemplate.template = selectedPreset; configToEdit.storageTemplate.template = selectedPreset;
}; };
let parsedTemplate = $derived(() => { let parsedTemplate = $derived(() => {
try { try {
return renderTemplate(config.storageTemplate.template); return renderTemplate(configToEdit.storageTemplate.template);
} catch { } catch {
return 'error'; return 'error';
} }
}); });
onDestroy(async () => {
if (saveOnClose) {
await handleSystemConfigSave({ storageTemplate: configToEdit.storageTemplate });
}
});
</script> </script>
<section class="dark:text-immich-dark-fg mt-2"> <section class="dark:text-immich-dark-fg mt-2">
@ -145,8 +135,8 @@
<SettingSwitch <SettingSwitch
title={$t('admin.storage_template_enable_description')} title={$t('admin.storage_template_enable_description')}
{disabled} {disabled}
bind:checked={config.storageTemplate.enabled} bind:checked={configToEdit.storageTemplate.enabled}
isEdited={!(config.storageTemplate.enabled === savedConfig.storageTemplate.enabled)} isEdited={!(configToEdit.storageTemplate.enabled === config.storageTemplate.enabled)}
/> />
{#if !minified} {#if !minified}
@ -154,14 +144,14 @@
title={$t('admin.storage_template_hash_verification_enabled')} title={$t('admin.storage_template_hash_verification_enabled')}
{disabled} {disabled}
subtitle={$t('admin.storage_template_hash_verification_enabled_description')} subtitle={$t('admin.storage_template_hash_verification_enabled_description')}
bind:checked={config.storageTemplate.hashVerificationEnabled} bind:checked={configToEdit.storageTemplate.hashVerificationEnabled}
isEdited={!( isEdited={!(
config.storageTemplate.hashVerificationEnabled === savedConfig.storageTemplate.hashVerificationEnabled configToEdit.storageTemplate.hashVerificationEnabled === config.storageTemplate.hashVerificationEnabled
)} )}
/> />
{/if} {/if}
{#if config.storageTemplate.enabled} {#if configToEdit.storageTemplate.enabled}
<hr /> <hr />
<h3 class="text-base font-medium text-primary">{$t('variables')}</h3> <h3 class="text-base font-medium text-primary">{$t('variables')}</h3>
@ -220,7 +210,7 @@
</label> </label>
<select <select
class="immich-form-input p-2 mt-2 text-sm rounded-lg bg-slate-200 hover:cursor-pointer dark:bg-gray-600" 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} disabled={disabled || !configToEdit.storageTemplate.enabled}
name="presets" name="presets"
id="preset-select" id="preset-select"
bind:value={selectedPreset} bind:value={selectedPreset}
@ -236,11 +226,11 @@
<div class="flex gap-2 align-bottom"> <div class="flex gap-2 align-bottom">
<SettingInputField <SettingInputField
label={$t('template')} label={$t('template')}
disabled={disabled || !config.storageTemplate.enabled} disabled={disabled || !configToEdit.storageTemplate.enabled}
required required
inputType={SettingInputFieldType.TEXT} inputType={SettingInputFieldType.TEXT}
bind:value={config.storageTemplate.template} bind:value={configToEdit.storageTemplate.template}
isEdited={!(config.storageTemplate.template === savedConfig.storageTemplate.template)} isEdited={!(configToEdit.storageTemplate.template === config.storageTemplate.template)}
/> />
<div class="flex-0"> <div class="flex-0">
@ -276,15 +266,8 @@
</div> </div>
{/if} {/if}
{#if minified} {#if !minified}
{@render children?.()} <SettingButtonsRow bind:configToEdit keys={['storageTemplate']} {disabled} />
{:else}
<SettingButtonsRow
onReset={(options) => onReset({ ...options, configKeys: ['storageTemplate'] })}
onSave={() => onSave({ storageTemplate: config.storageTemplate })}
showResetToDefault={!isEqual(savedConfig.storageTemplate, defaultConfig.storageTemplate) && !minified}
{disabled}
/>
{/if} {/if}
</div> </div>
{/await} {/await}

View File

@ -3,6 +3,7 @@
import SettingTextarea from '$lib/components/shared-components/settings/setting-textarea.svelte'; import SettingTextarea from '$lib/components/shared-components/settings/setting-textarea.svelte';
import FormatMessage from '$lib/elements/FormatMessage.svelte'; import FormatMessage from '$lib/elements/FormatMessage.svelte';
import EmailTemplatePreviewModal from '$lib/modals/EmailTemplatePreviewModal.svelte'; import EmailTemplatePreviewModal from '$lib/modals/EmailTemplatePreviewModal.svelte';
import { systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { type SystemConfigDto, type SystemConfigTemplateEmailsDto, getNotificationTemplateAdmin } from '@immich/sdk'; import { type SystemConfigDto, type SystemConfigTemplateEmailsDto, getNotificationTemplateAdmin } from '@immich/sdk';
import { Button, Icon, LoadingSpinner, modalManager } from '@immich/ui'; import { Button, Icon, LoadingSpinner, modalManager } from '@immich/ui';
@ -11,11 +12,10 @@
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
interface Props { interface Props {
savedConfig: SystemConfigDto;
config: SystemConfigDto; config: SystemConfigDto;
} }
let { savedConfig, config = $bindable() }: Props = $props(); let { config = $bindable() }: Props = $props();
let loadingPreview = $state(false); let loadingPreview = $state(false);
@ -53,7 +53,7 @@
]; ];
const isEdited = (templateKey: keyof SystemConfigTemplateEmailsDto) => const isEdited = (templateKey: keyof SystemConfigTemplateEmailsDto) =>
config.templates.email[templateKey] !== savedConfig.templates.email[templateKey]; config.templates.email[templateKey] !== systemConfigManager.value.templates.email[templateKey];
const onsubmit = (event: Event) => { const onsubmit = (event: Event) => {
event.preventDefault(); event.preventDefault();

View File

@ -1,46 +1,28 @@
<script lang="ts"> <script lang="ts">
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte'; import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingTextarea from '$lib/components/shared-components/settings/setting-textarea.svelte'; import SettingTextarea from '$lib/components/shared-components/settings/setting-textarea.svelte';
import type { SystemConfigDto } from '@immich/sdk'; import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { isEqual } from 'lodash-es';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from './admin-settings';
interface Props { const disabled = $featureFlags.configFile;
savedConfig: SystemConfigDto; const config = $derived(systemConfigManager.value);
defaultConfig: SystemConfigDto; let configToEdit = $state(systemConfigManager.cloneValue());
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> </script>
<div> <div>
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<form autocomplete="off" {onsubmit}> <form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
<div class="ms-4 mt-4 flex flex-col gap-4"> <div class="ms-4 mt-4 flex flex-col gap-4">
<SettingTextarea <SettingTextarea
{disabled} {disabled}
label={$t('admin.theme_custom_css_settings')} label={$t('admin.theme_custom_css_settings')}
description={$t('admin.theme_custom_css_settings_description')} description={$t('admin.theme_custom_css_settings_description')}
bind:value={config.theme.customCss} bind:value={configToEdit.theme.customCss}
isEdited={config.theme.customCss !== savedConfig.theme.customCss} isEdited={configToEdit.theme.customCss !== config.theme.customCss}
/> />
<SettingButtonsRow <SettingButtonsRow bind:configToEdit keys={['theme']} {disabled} />
onReset={(options) => onReset({ ...options, configKeys: ['theme'] })}
onSave={() => onSave({ theme: config.theme })}
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
{disabled}
/>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,35 +1,26 @@
<script lang="ts"> <script lang="ts">
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte'; import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingInputField 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 SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { SettingInputFieldType } from '$lib/constants'; import { SettingInputFieldType } from '$lib/constants';
import type { SystemConfigDto } from '@immich/sdk'; import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { isEqual } from 'lodash-es';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import type { SettingsResetEvent, SettingsSaveEvent } from './admin-settings';
interface Props { const disabled = $featureFlags.configFile;
savedConfig: SystemConfigDto; const config = $derived(systemConfigManager.value);
defaultConfig: SystemConfigDto; let configToEdit = $state(systemConfigManager.cloneValue());
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> </script>
<div> <div>
<div in:fade={{ duration: 500 }}> <div in:fade={{ duration: 500 }}>
<form autocomplete="off" {onsubmit}> <form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
<div class="ms-4 mt-4 flex flex-col gap-4"> <div class="ms-4 mt-4 flex flex-col gap-4">
<SettingSwitch title={$t('admin.trash_enabled_description')} {disabled} bind:checked={config.trash.enabled} /> <SettingSwitch
title={$t('admin.trash_enabled_description')}
{disabled}
bind:checked={configToEdit.trash.enabled}
/>
<hr /> <hr />
@ -37,18 +28,13 @@
inputType={SettingInputFieldType.NUMBER} inputType={SettingInputFieldType.NUMBER}
label={$t('admin.trash_number_of_days')} label={$t('admin.trash_number_of_days')}
description={$t('admin.trash_number_of_days_description')} description={$t('admin.trash_number_of_days_description')}
bind:value={config.trash.days} bind:value={configToEdit.trash.days}
required={true} required={true}
disabled={disabled || !config.trash.enabled} disabled={disabled || !configToEdit.trash.enabled}
isEdited={config.trash.days !== savedConfig.trash.days} isEdited={configToEdit.trash.days !== config.trash.days}
/> />
<SettingButtonsRow <SettingButtonsRow bind:configToEdit keys={['trash']} {disabled} />
onReset={(options) => onReset({ ...options, configKeys: ['trash'] })}
onSave={() => onSave({ trash: config.trash })}
showResetToDefault={!isEqual(savedConfig.trash, defaultConfig.trash)}
{disabled}
/>
</div> </div>
</form> </form>
</div> </div>

View File

@ -1,24 +1,15 @@
<script lang="ts"> <script lang="ts">
import { type SystemConfigDto } from '@immich/sdk';
import { isEqual } from 'lodash-es';
import { fade } from 'svelte/transition'; 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 SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte'; import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
import { SettingInputFieldType } from '$lib/constants'; import { SettingInputFieldType } from '$lib/constants';
import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
interface Props { const disabled = $featureFlags.configFile;
savedConfig: SystemConfigDto; const config = $derived(systemConfigManager.value);
defaultConfig: SystemConfigDto; let configToEdit = $state(systemConfigManager.cloneValue());
config: SystemConfigDto;
disabled?: boolean;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
}
let { savedConfig, defaultConfig, config = $bindable(), disabled = false, onReset, onSave }: Props = $props();
</script> </script>
<div> <div>
@ -30,18 +21,13 @@
min={1} min={1}
label={$t('admin.user_delete_delay_settings')} label={$t('admin.user_delete_delay_settings')}
description={$t('admin.user_delete_delay_settings_description')} description={$t('admin.user_delete_delay_settings_description')}
bind:value={config.user.deleteDelay} bind:value={configToEdit.user.deleteDelay}
isEdited={config.user.deleteDelay !== savedConfig.user.deleteDelay} isEdited={configToEdit.user.deleteDelay !== config.user.deleteDelay}
/> />
</div> </div>
<div class="ms-4"> <div class="ms-4">
<SettingButtonsRow <SettingButtonsRow bind:configToEdit keys={['user']} {disabled} />
onReset={(options) => onReset({ ...options, configKeys: ['user'] })}
onSave={() => onSave({ user: config.user })}
showResetToDefault={!isEqual(savedConfig.user, defaultConfig.user)}
{disabled}
/>
</div> </div>
</form> </form>
</div> </div>

View File

@ -2,14 +2,3 @@ import type { ResetOptions } from '$lib/utils/dipatch';
import type { SystemConfigDto } from '@immich/sdk'; import type { SystemConfigDto } from '@immich/sdk';
export type SettingsResetOptions = ResetOptions & { configKeys: Array<keyof SystemConfigDto> }; export type SettingsResetOptions = ResetOptions & { configKeys: Array<keyof SystemConfigDto> };
export type SettingsResetEvent = (options: SettingsResetOptions) => void;
export type SettingsSaveEvent = (config: Partial<SystemConfigDto>) => void;
export type SettingsComponentProps = {
disabled?: boolean;
defaultConfig: SystemConfigDto;
config: SystemConfigDto;
savedConfig: SystemConfigDto;
onReset: SettingsResetEvent;
onSave: SettingsSaveEvent;
};

View File

@ -11,7 +11,7 @@
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
import { cancelMultiselect } from '$lib/utils/asset-utils'; import { cancelMultiselect } from '$lib/utils/asset-utils';
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader'; import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';

View File

@ -4,7 +4,7 @@
import { AssetAction } from '$lib/constants'; import { AssetAction } from '$lib/constants';
import Portal from '$lib/elements/Portal.svelte'; import Portal from '$lib/elements/Portal.svelte';
import { showDeleteModal } from '$lib/stores/preferences.store'; import { showDeleteModal } from '$lib/stores/preferences.store';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { toTimelineAsset } from '$lib/utils/timeline-util'; import { toTimelineAsset } from '$lib/utils/timeline-util';
import { deleteAssets, type AssetResponseDto } from '@immich/sdk'; import { deleteAssets, type AssetResponseDto } from '@immich/sdk';

View File

@ -25,7 +25,7 @@
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte'; import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { photoZoomState } from '$lib/stores/zoom-image.store'; import { photoZoomState } from '$lib/stores/zoom-image.store';
import { getAssetJobName, getSharedLink } from '$lib/utils'; import { getAssetJobName, getSharedLink } from '$lib/utils';

View File

@ -11,7 +11,7 @@
import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
import { boundingBoxesArray } from '$lib/stores/people.store'; import { boundingBoxesArray } from '$lib/stores/people.store';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { preferences, user } from '$lib/stores/user.store'; import { preferences, user } from '$lib/stores/user.store';
import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils'; import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils';
import { delay, getDimensions } from '$lib/utils/asset-utils'; import { delay, getDimensions } from '$lib/utils/asset-utils';

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { getJobName } from '$lib/utils'; import { getJobName } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { JobCommand, JobName, sendJobCommand, type AllJobStatusResponseDto, type JobCommandDto } from '@immich/sdk'; import { JobCommand, JobName, sendJobCommand, type AllJobStatusResponseDto, type JobCommandDto } from '@immich/sdk';
@ -12,10 +12,10 @@
mdiFolderMove, mdiFolderMove,
mdiImageSearch, mdiImageSearch,
mdiLibraryShelves, mdiLibraryShelves,
mdiOcr,
mdiTable, mdiTable,
mdiTagFaces, mdiTagFaces,
mdiVideo, mdiVideo,
mdiOcr,
} from '@mdi/js'; } from '@mdi/js';
import type { Component } from 'svelte'; import type { Component } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { OnboardingRole } from '$lib/models/onboarding-role'; import { OnboardingRole } from '$lib/models/onboarding-role';
import { serverConfig } from '$lib/stores/server-config.store'; import { serverConfig } from '$lib/stores/system-config-manager.svelte';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { Logo } from '@immich/ui'; import { Logo } from '@immich/ui';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';

View File

@ -1,17 +1,14 @@
<script lang="ts"> <script lang="ts">
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { systemConfig } from '$lib/stores/server-config.store'; import { handleSystemConfigSave } from '$lib/services/system-config.service';
import { updateConfig } from '@immich/sdk'; import { systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { get } from 'svelte/store';
const configToEdit = $state(systemConfigManager.cloneValue());
onDestroy(async () => { onDestroy(async () => {
const cfg = get(systemConfig); await handleSystemConfigSave({ map: configToEdit.map, newVersionCheck: configToEdit.newVersionCheck });
await updateConfig({
systemConfigDto: cfg,
});
}); });
</script> </script>
@ -20,16 +17,14 @@
{$t('onboarding_privacy_description')} {$t('onboarding_privacy_description')}
</p> </p>
{#if $systemConfig} <SettingSwitch
<SettingSwitch title={$t('admin.map_settings')}
title={$t('admin.map_settings')} subtitle={$t('admin.map_implications')}
subtitle={$t('admin.map_implications')} bind:checked={configToEdit.map.enabled}
bind:checked={$systemConfig.map.enabled} />
/> <SettingSwitch
<SettingSwitch title={$t('admin.version_check_settings')}
title={$t('admin.version_check_settings')} subtitle={$t('admin.version_check_implications')}
subtitle={$t('admin.version_check_implications')} bind:checked={configToEdit.newVersionCheck.enabled}
bind:checked={$systemConfig.newVersionCheck.enabled} />
/>
{/if}
</div> </div>

View File

@ -1,20 +1,7 @@
<script lang="ts"> <script lang="ts">
import AdminSettings from '$lib/components/admin-settings/AdminSettings.svelte';
import StorageTemplateSettings from '$lib/components/admin-settings/StorageTemplateSettings.svelte'; import StorageTemplateSettings from '$lib/components/admin-settings/StorageTemplateSettings.svelte';
import FormatMessage from '$lib/elements/FormatMessage.svelte'; import FormatMessage from '$lib/elements/FormatMessage.svelte';
import { featureFlags } from '$lib/stores/server-config.store';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { getConfig, type SystemConfigDto } from '@immich/sdk';
import { onDestroy, onMount } from 'svelte';
let config: SystemConfigDto | undefined = $state();
let adminSettingsComponent = $state<ReturnType<typeof AdminSettings>>();
onMount(async () => {
config = await getConfig();
});
onDestroy(() => adminSettingsComponent?.handleSave({ storageTemplate: config?.storageTemplate }));
</script> </script>
<div class="flex flex-col"> <div class="flex flex-col">
@ -26,22 +13,7 @@
</FormatMessage> </FormatMessage>
</p> </p>
{#if config && $user} {#if $user}
<AdminSettings bind:config bind:this={adminSettingsComponent}> <StorageTemplateSettings minified duration={0} saveOnClose />
{#snippet children({ defaultConfig, savedConfig })}
{#if config}
<StorageTemplateSettings
minified
disabled={$featureFlags.configFile}
{config}
{defaultConfig}
{savedConfig}
onSave={(config) => adminSettingsComponent?.handleSave(config)}
onReset={(options) => adminSettingsComponent?.handleReset(options)}
duration={0}
/>
{/if}
{/snippet}
</AdminSettings>
{/if} {/if}
</div> </div>

View File

@ -10,7 +10,7 @@
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { showDeleteModal } from '$lib/stores/preferences.store'; import { showDeleteModal } from '$lib/stores/preferences.store';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
import { deleteAssets } from '$lib/utils/actions'; import { deleteAssets } from '$lib/utils/actions';
import { archiveAssets, cancelMultiselect } from '$lib/utils/asset-utils'; import { archiveAssets, cancelMultiselect } from '$lib/utils/asset-utils';

View File

@ -14,7 +14,7 @@
import { themeManager } from '$lib/managers/theme-manager.svelte'; import { themeManager } from '$lib/managers/theme-manager.svelte';
import MapSettingsModal from '$lib/modals/MapSettingsModal.svelte'; import MapSettingsModal from '$lib/modals/MapSettingsModal.svelte';
import { mapSettings } from '$lib/stores/preferences.store'; import { mapSettings } from '$lib/stores/preferences.store';
import { serverConfig } from '$lib/stores/server-config.store'; import { serverConfig } from '$lib/stores/system-config-manager.svelte';
import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils'; import { getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
import { getMapMarkers, type MapMarkerResponseDto } from '@immich/sdk'; import { getMapMarkers, type MapMarkerResponseDto } from '@immich/sdk';
import { Icon, modalManager } from '@immich/ui'; import { Icon, modalManager } from '@immich/ui';

View File

@ -13,8 +13,8 @@
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { mobileDevice } from '$lib/stores/mobile-device.svelte'; import { mobileDevice } from '$lib/stores/mobile-device.svelte';
import { notificationManager } from '$lib/stores/notification-manager.svelte'; import { notificationManager } from '$lib/stores/notification-manager.svelte';
import { featureFlags } from '$lib/stores/server-config.store';
import { sidebarStore } from '$lib/stores/sidebar.svelte'; import { sidebarStore } from '$lib/stores/sidebar.svelte';
import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { Button, IconButton, Logo } from '@immich/ui'; import { Button, IconButton, Logo } from '@immich/ui';
import { mdiBellBadge, mdiBellOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js'; import { mdiBellBadge, mdiBellOutline, mdiMagnify, mdiMenu, mdiTrayArrowUp } from '@mdi/js';

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import RadioButton from '$lib/elements/RadioButton.svelte'; import RadioButton from '$lib/elements/RadioButton.svelte';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
interface Props { interface Props {
@ -72,7 +72,7 @@
{:else if queryType === 'ocr'} {:else if queryType === 'ocr'}
<label for="ocr-input" class="immich-form-label">{$t('search_by_ocr')}</label> <label for="ocr-input" class="immich-form-label">{$t('search_by_ocr')}</label>
<input <input
class="immich-form-input hover:cursor-text w-full !mt-1" class="immich-form-input hover:cursor-text w-full mt-1!"
type="text" type="text"
id="ocr-input" id="ocr-input"
name="ocr" name="ocr"

View File

@ -0,0 +1,57 @@
<script lang="ts">
import { handleSystemConfigSave } from '$lib/services/system-config.service';
import { systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import type { SystemConfigDto } from '@immich/sdk';
import { Button, toastManager } from '@immich/ui';
import { isEqual, pick } from 'lodash-es';
import { t } from 'svelte-i18n';
type Props = {
disabled?: boolean;
keys: Array<keyof SystemConfigDto>;
configToEdit: SystemConfigDto;
onBeforeSave?: () => Promise<boolean>;
};
let { disabled, keys, configToEdit = $bindable(), onBeforeSave }: Props = $props();
const showResetToDefault = $derived(
!isEqual(pick(systemConfigManager.value, keys), pick(systemConfigManager.defaultValue, keys)),
);
const handleReset = () => {
configToEdit = systemConfigManager.cloneValue();
toastManager.info($t('admin.reset_settings_to_recent_saved'));
};
const handleResetToDefault = () => {
const defaultConfig = systemConfigManager.cloneDefaultValue();
configToEdit = { ...configToEdit, ...pick(defaultConfig, keys) };
toastManager.info($t('admin.reset_settings_to_default'));
};
const handleSave = async () => {
const shouldSave = await onBeforeSave?.();
if (shouldSave ?? true) {
await handleSystemConfigSave(pick(configToEdit, keys));
}
};
</script>
<div class="mt-8 flex justify-between gap-2">
<div class="left">
{#if showResetToDefault}
<Button variant="ghost" shape="round" size="small" onclick={handleResetToDefault}>
{$t('reset_to_default')}
</Button>
{/if}
</div>
<div class="flex gap-1">
<Button shape="round" {disabled} size="small" color="secondary" onclick={handleReset}>{$t('reset')}</Button>
<Button shape="round" type="submit" {disabled} size="small" onclick={handleSave}>{$t('save')}</Button>
</div>
</div>

View File

@ -1,31 +0,0 @@
<script lang="ts">
import type { ResetOptions } from '$lib/utils/dipatch';
import { Button } from '@immich/ui';
import { t } from 'svelte-i18n';
interface Props {
showResetToDefault?: boolean;
disabled?: boolean;
onReset: (options: ResetOptions) => void;
onSave: () => void;
}
let { showResetToDefault = true, disabled = false, onReset, onSave }: Props = $props();
</script>
<div class="mt-8 flex justify-between gap-2">
<div class="left">
{#if showResetToDefault}
<Button variant="ghost" shape="round" size="small" onclick={() => onReset({ default: true })}
>{$t('reset_to_default')}</Button
>
{/if}
</div>
<div class="flex gap-1">
<Button shape="round" {disabled} size="small" color="secondary" onclick={() => onReset({ default: false })}
>{$t('reset')}</Button
>
<Button shape="round" type="submit" {disabled} size="small" onclick={() => onSave()}>{$t('save')}</Button>
</div>
</div>

View File

@ -4,7 +4,7 @@
import RecentAlbums from '$lib/components/shared-components/side-bar/recent-albums.svelte'; import RecentAlbums from '$lib/components/shared-components/side-bar/recent-albums.svelte';
import Sidebar from '$lib/components/sidebar/sidebar.svelte'; import Sidebar from '$lib/components/sidebar/sidebar.svelte';
import { recentAlbumsDropdown } from '$lib/stores/preferences.store'; import { recentAlbumsDropdown } from '$lib/stores/preferences.store';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { preferences } from '$lib/stores/user.store'; import { preferences } from '$lib/stores/user.store';
import { import {
mdiAccount, mdiAccount,

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import DeleteAssetDialog from '$lib/components/photos-page/delete-asset-dialog.svelte'; import DeleteAssetDialog from '$lib/components/photos-page/delete-asset-dialog.svelte';
import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte'; import { getAssetControlContext } from '$lib/components/timeline/AssetSelectControlBar.svelte';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { type OnDelete, type OnUndoDelete, deleteAssets } from '$lib/utils/actions'; import { type OnDelete, type OnUndoDelete, deleteAssets } from '$lib/utils/actions';
import { IconButton } from '@immich/ui'; import { IconButton } from '@immich/ui';
import { mdiDeleteForeverOutline, mdiDeleteOutline, mdiTimerSand } from '@mdi/js'; import { mdiDeleteForeverOutline, mdiDeleteOutline, mdiTimerSand } from '@mdi/js';

View File

@ -15,7 +15,7 @@
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { showDeleteModal } from '$lib/stores/preferences.store'; import { showDeleteModal } from '$lib/stores/preferences.store';
import { searchStore } from '$lib/stores/search.svelte'; import { searchStore } from '$lib/stores/search.svelte';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
import { deleteAssets, updateStackedAssetInTimeline } from '$lib/utils/actions'; import { deleteAssets, updateStackedAssetInTimeline } from '$lib/utils/actions';
import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils'; import { archiveAssets, cancelMultiselect, selectAllAssets, stackAssets } from '$lib/utils/asset-utils';

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { oauth } from '$lib/utils'; import { oauth } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { type UserAdminResponseDto } from '@immich/sdk'; import { type UserAdminResponseDto } from '@immich/sdk';

View File

@ -7,7 +7,7 @@
import UserPurchaseSettings from '$lib/components/user-settings-page/user-purchase-settings.svelte'; import UserPurchaseSettings from '$lib/components/user-settings-page/user-purchase-settings.svelte';
import UserUsageStatistic from '$lib/components/user-settings-page/user-usage-statistic.svelte'; import UserUsageStatistic from '$lib/components/user-settings-page/user-usage-statistic.svelte';
import { OpenSettingQueryParameterValue, QueryParameter } from '$lib/constants'; import { OpenSettingQueryParameterValue, QueryParameter } from '$lib/constants';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { oauth } from '$lib/utils'; import { oauth } from '$lib/utils';
import { type ApiKeyResponseDto, type SessionResponseDto } from '@immich/sdk'; import { type ApiKeyResponseDto, type SessionResponseDto } from '@immich/sdk';
@ -20,9 +20,9 @@
mdiDevices, mdiDevices,
mdiDownload, mdiDownload,
mdiFeatureSearchOutline, mdiFeatureSearchOutline,
mdiFormTextboxPassword,
mdiKeyOutline, mdiKeyOutline,
mdiLockSmart, mdiLockSmart,
mdiFormTextboxPassword,
mdiServerOutline, mdiServerOutline,
mdiTwoFactorAuthentication, mdiTwoFactorAuthentication,
} from '@mdi/js'; } from '@mdi/js';

View File

@ -1,5 +1,11 @@
import type { ThemeSetting } from '$lib/managers/theme-manager.svelte'; import type { ThemeSetting } from '$lib/managers/theme-manager.svelte';
import type { AlbumResponseDto, LoginResponseDto, SharedLinkResponseDto, UserAdminResponseDto } from '@immich/sdk'; import type {
AlbumResponseDto,
LoginResponseDto,
SharedLinkResponseDto,
SystemConfigDto,
UserAdminResponseDto,
} from '@immich/sdk';
export type Events = { export type Events = {
AppInit: []; AppInit: [];
@ -19,6 +25,8 @@ export type Events = {
UserAdminUpdate: [UserAdminResponseDto]; UserAdminUpdate: [UserAdminResponseDto];
UserAdminDelete: [UserAdminResponseDto]; UserAdminDelete: [UserAdminResponseDto];
UserAdminRestore: [UserAdminResponseDto]; UserAdminRestore: [UserAdminResponseDto];
SystemConfigUpdate: [SystemConfigDto];
}; };
type Listener<EventMap extends Record<string, unknown[]>, K extends keyof EventMap> = (...params: EventMap[K]) => void; type Listener<EventMap extends Record<string, unknown[]>, K extends keyof EventMap> = (...params: EventMap[K]) => void;

View File

@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { resetPinCode } from '@immich/sdk'; import { resetPinCode } from '@immich/sdk';
import { import {

View File

@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import { handleCreateUserAdmin } from '$lib/services/user-admin.service'; import { handleCreateUserAdmin } from '$lib/services/user-admin.service';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { userInteraction } from '$lib/stores/user.svelte'; import { userInteraction } from '$lib/stores/user.svelte';
import { ByteUnit, convertToBytes } from '$lib/utils/byte-units'; import { ByteUnit, convertToBytes } from '$lib/utils/byte-units';
import { import {

View File

@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import FormatMessage from '$lib/elements/FormatMessage.svelte'; import FormatMessage from '$lib/elements/FormatMessage.svelte';
import { handleDeleteUserAdmin } from '$lib/services/user-admin.service'; import { handleDeleteUserAdmin } from '$lib/services/user-admin.service';
import { serverConfig } from '$lib/stores/server-config.store'; import { serverConfig } from '$lib/stores/system-config-manager.svelte';
import { type UserAdminResponseDto } from '@immich/sdk'; import { type UserAdminResponseDto } from '@immich/sdk';
import { Alert, Checkbox, ConfirmModal, Field, Input, Label, Text } from '@immich/ui'; import { Alert, Checkbox, ConfirmModal, Field, Input, Label, Text } from '@immich/ui';
import { mdiTrashCanOutline } from '@mdi/js'; import { mdiTrashCanOutline } from '@mdi/js';

View File

@ -3,7 +3,7 @@ import { AppRoute } from '$lib/constants';
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte';
import QrCodeModal from '$lib/modals/QrCodeModal.svelte'; import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
import { serverConfig } from '$lib/stores/server-config.store'; import { serverConfig } from '$lib/stores/system-config-manager.svelte';
import { copyToClipboard } from '$lib/utils'; import { copyToClipboard } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
@ -91,7 +91,6 @@ export const handleUpdateSharedLink = async (sharedLink: SharedLinkResponseDto,
const response = await updateSharedLink({ id: sharedLink.id, sharedLinkEditDto: dto }); const response = await updateSharedLink({ id: sharedLink.id, sharedLinkEditDto: dto });
eventManager.emit('SharedLinkUpdate', { album: sharedLink.album, ...response }); eventManager.emit('SharedLinkUpdate', { album: sharedLink.album, ...response });
toastManager.success($t('saved')); toastManager.success($t('saved'));
return true; return true;

View File

@ -0,0 +1,102 @@
import { downloadManager } from '$lib/managers/download-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte';
import { type FeatureFlags } from '$lib/stores/system-config-manager.svelte';
import type { ActionItem } from '$lib/types';
import { copyToClipboard } from '$lib/utils';
import { downloadBlob } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n';
import { getConfig, updateConfig, type SystemConfigDto } from '@immich/sdk';
import { toastManager } from '@immich/ui';
import { mdiContentCopy, mdiDownload, mdiUpload } from '@mdi/js';
import { isEqual } from 'lodash-es';
import type { MessageFormatter } from 'svelte-i18n';
export const getSystemConfigActions = ($t: MessageFormatter, $featureFlags: FeatureFlags, config: SystemConfigDto) => {
const CopyToClipboard: ActionItem = {
title: $t('copy_to_clipboard'),
icon: mdiContentCopy,
onSelect: () => void handleCopyToClipboard(config),
};
const Download: ActionItem = {
title: $t('export_as_json'),
icon: mdiDownload,
onSelect: () => handleDownloadConfig(config),
};
const Upload: ActionItem = {
title: $t('import_from_json'),
icon: mdiUpload,
$if: () => !$featureFlags.configFile,
onSelect: () => handleUploadConfig(),
};
return { CopyToClipboard, Download, Upload };
};
export const handleSystemConfigSave = async (update: Partial<SystemConfigDto>) => {
const $t = await getFormatter();
const config = await getConfig();
const systemConfigDto = { ...config, ...update };
if (isEqual(config, systemConfigDto)) {
return;
}
try {
const newConfig = await updateConfig({ systemConfigDto });
eventManager.emit('SystemConfigUpdate', newConfig);
toastManager.success($t('settings_saved'));
} catch (error) {
handleError(error, $t('errors.unable_to_save_settings'));
}
};
// https://stackoverflow.com/questions/16167581/sort-object-properties-and-json-stringify/43636793#43636793
const jsonReplacer = (_key: string, value: unknown) =>
value instanceof Object && !Array.isArray(value)
? Object.keys(value)
.sort()
// eslint-disable-next-line unicorn/no-array-reduce
.reduce((sorted: { [key: string]: unknown }, key) => {
sorted[key] = (value as { [key: string]: unknown })[key];
return sorted;
}, {})
: value;
export const handleCopyToClipboard = async (config: SystemConfigDto) => {
await copyToClipboard(JSON.stringify(config, jsonReplacer, 2));
};
export const handleDownloadConfig = (config: SystemConfigDto) => {
const blob = new Blob([JSON.stringify(config, jsonReplacer, 2)], { type: 'application/json' });
const downloadKey = 'immich-config.json';
downloadManager.add(downloadKey, blob.size);
downloadManager.update(downloadKey, blob.size);
downloadBlob(blob, downloadKey);
setTimeout(() => downloadManager.clear(downloadKey), 5000);
};
export const handleUploadConfig = () => {
const input = globalThis.document.createElement('input');
input.setAttribute('type', 'file');
input.setAttribute('accept', 'json');
input.setAttribute('style', 'display: none');
input.addEventListener('change', ({ target }) => {
const file = (target as HTMLInputElement).files?.[0];
if (!file) {
return;
}
const reader = async () => {
const text = await file.text();
const newConfig = JSON.parse(text);
await handleSystemConfigSave(newConfig);
};
reader().catch((error) => console.error('Error handling JSON config upload', error));
globalThis.document.append(input);
});
input.remove();
};

View File

@ -5,7 +5,7 @@ import UserCreateModal from '$lib/modals/UserCreateModal.svelte';
import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte'; import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte';
import UserEditModal from '$lib/modals/UserEditModal.svelte'; import UserEditModal from '$lib/modals/UserEditModal.svelte';
import UserRestoreConfirmModal from '$lib/modals/UserRestoreConfirmModal.svelte'; import UserRestoreConfirmModal from '$lib/modals/UserRestoreConfirmModal.svelte';
import { serverConfig } from '$lib/stores/server-config.store'; import { serverConfig } from '$lib/stores/system-config-manager.svelte';
import { user as authUser } from '$lib/stores/user.store'; import { user as authUser } from '$lib/stores/user.store';
import type { ActionItem } from '$lib/types'; import type { ActionItem } from '$lib/types';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';

View File

@ -1,11 +1,14 @@
import { eventManager } from '$lib/managers/event-manager.svelte';
import { import {
getConfig, getConfig,
getConfigDefaults,
getServerConfig, getServerConfig,
getServerFeatures, getServerFeatures,
type ServerConfigDto, type ServerConfigDto,
type ServerFeaturesDto, type ServerFeaturesDto,
type SystemConfigDto, type SystemConfigDto,
} from '@immich/sdk'; } from '@immich/sdk';
import { cloneDeep } from 'lodash-es';
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
export type FeatureFlags = ServerFeaturesDto & { loaded: boolean }; export type FeatureFlags = ServerFeaturesDto & { loaded: boolean };
@ -45,8 +48,55 @@ export const serverConfig = writable<ServerConfig>({
publicUsers: true, publicUsers: true,
}); });
export type SystemConfig = SystemConfigDto & { loaded: boolean }; class SystemConfigManager {
export const systemConfig = writable<SystemConfig>(); #value?: SystemConfigDto = $state();
#defaultValue?: SystemConfigDto = $state();
constructor() {
eventManager.on('SystemConfigUpdate', (config) => (this.#value = config));
}
get value() {
if (!this.#value) {
throw new Error('System config dto must be initialized first');
}
return this.#value;
}
set value(config: SystemConfigDto) {
this.#value = config;
}
get defaultValue() {
if (!this.#defaultValue) {
throw new Error('System config dto must be initialized first');
}
return this.#defaultValue;
}
cloneValue() {
return cloneDeep(this.value);
}
cloneDefaultValue() {
return cloneDeep(this.defaultValue);
}
async init() {
await this.#loadConfig();
await this.#loadDefault();
}
async #loadConfig() {
this.#value = await getConfig();
}
async #loadDefault() {
this.#defaultValue = await getConfigDefaults();
}
}
export const retrieveServerConfig = async () => { export const retrieveServerConfig = async () => {
const [flags, config] = await Promise.all([getServerFeatures(), getServerConfig()]); const [flags, config] = await Promise.all([getServerFeatures(), getServerConfig()]);
@ -55,7 +105,4 @@ export const retrieveServerConfig = async () => {
serverConfig.update(() => ({ ...config, loaded: true })); serverConfig.update(() => ({ ...config, loaded: true }));
}; };
export const retrieveSystemConfig = async () => { export const systemConfigManager = new SystemConfigManager();
const config = await getConfig();
systemConfig.update(() => ({ ...config, loaded: true }));
};

View File

@ -1,6 +1,6 @@
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 { ImmichProduct } from '$lib/constants'; import type { ImmichProduct } from '$lib/constants';
import { serverConfig } from '$lib/stores/server-config.store'; import { serverConfig } from '$lib/stores/system-config-manager.svelte';
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';

View File

@ -1,4 +1,4 @@
import { retrieveServerConfig } from '$lib/stores/server-config.store'; import { retrieveServerConfig } from '$lib/stores/system-config-manager.svelte';
import { initLanguage } from '$lib/utils'; import { initLanguage } from '$lib/utils';
import { defaults } from '@immich/sdk'; import { defaults } from '@immich/sdk';
import { memoize } from 'lodash-es'; import { memoize } from 'lodash-es';

View File

@ -39,8 +39,8 @@
import { handleDeleteAlbum, handleDownloadAlbum } from '$lib/services/album.service'; import { handleDeleteAlbum, handleDownloadAlbum } from '$lib/services/album.service';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { featureFlags } from '$lib/stores/server-config.store';
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { preferences, user } from '$lib/stores/user.store'; import { preferences, user } from '$lib/stores/user.store';
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
import { cancelMultiselect } from '$lib/utils/asset-utils'; import { cancelMultiselect } from '$lib/utils/asset-utils';

View File

@ -4,7 +4,7 @@
import { AppRoute, timeToLoadTheMap } from '$lib/constants'; import { AppRoute, timeToLoadTheMap } from '$lib/constants';
import Portal from '$lib/elements/Portal.svelte'; import Portal from '$lib/elements/Portal.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
import { delay } from '$lib/utils/asset-utils'; import { delay } from '$lib/utils/asset-utils';
import { navigate } from '$lib/utils/navigation'; import { navigate } from '$lib/utils/navigation';

View File

@ -25,7 +25,7 @@
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { lang, locale } from '$lib/stores/preferences.store'; import { lang, locale } from '$lib/stores/preferences.store';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { preferences } from '$lib/stores/user.store'; import { preferences } from '$lib/stores/user.store';
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
import { cancelMultiselect } from '$lib/utils/asset-utils'; import { cancelMultiselect } from '$lib/utils/asset-utils';

View File

@ -11,7 +11,7 @@
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { featureFlags, serverConfig } from '$lib/stores/server-config.store'; import { featureFlags, serverConfig } from '$lib/stores/system-config-manager.svelte';
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { emptyTrash, restoreTrash } from '@immich/sdk'; import { emptyTrash, restoreTrash } from '@immich/sdk';

View File

@ -9,7 +9,7 @@
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte'; import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { locale } from '$lib/stores/preferences.store'; import { locale } from '$lib/stores/preferences.store';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/system-config-manager.svelte';
import { stackAssets } from '$lib/utils/asset-utils'; import { stackAssets } from '$lib/utils/asset-utils';
import { suggestDuplicate } from '$lib/utils/duplicate-utils'; import { suggestDuplicate } from '$lib/utils/duplicate-utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
@ -272,7 +272,7 @@
handleResolve(duplicates[duplicatesIndex].duplicateId, duplicateAssetIds, trashIds)} handleResolve(duplicates[duplicatesIndex].duplicateId, duplicateAssetIds, trashIds)}
onStack={(assets) => handleStack(duplicates[duplicatesIndex].duplicateId, assets)} onStack={(assets) => handleStack(duplicates[duplicatesIndex].duplicateId, assets)}
/> />
<div class="max-w-256 mx-auto mb-16"> <div class="max-w-5xl mx-auto mb-16">
<div class="flex mb-4 sm:px-6 w-full place-content-center justify-between items-center place-items-center"> <div class="flex mb-4 sm:px-6 w-full place-content-center justify-between items-center place-items-center">
<div class="flex text-xs text-black"> <div class="flex text-xs text-black">
<Button <Button

View File

@ -9,7 +9,7 @@
import UploadPanel from '$lib/components/shared-components/upload-panel.svelte'; import UploadPanel from '$lib/components/shared-components/upload-panel.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte';
import VersionAnnouncementModal from '$lib/modals/VersionAnnouncementModal.svelte'; import VersionAnnouncementModal from '$lib/modals/VersionAnnouncementModal.svelte';
import { serverConfig } from '$lib/stores/server-config.store'; import { serverConfig } from '$lib/stores/system-config-manager.svelte';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { import {
closeWebsocketConnection, closeWebsocketConnection,

View File

@ -1,5 +1,5 @@
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { serverConfig } from '$lib/stores/server-config.store'; import { serverConfig } from '$lib/stores/system-config-manager.svelte';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
import { init } from '$lib/utils/server'; import { init } from '$lib/utils/server';

View File

@ -0,0 +1,6 @@
import { systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import type { LayoutLoad } from './$types';
export const load = (async () => {
await systemConfigManager.init();
}) satisfies LayoutLoad;

View File

@ -1,6 +1,4 @@
<script lang="ts"> <script lang="ts">
import type { SettingsComponentProps } from '$lib/components/admin-settings/admin-settings';
import AdminSettings from '$lib/components/admin-settings/AdminSettings.svelte';
import AuthSettings from '$lib/components/admin-settings/AuthSettings.svelte'; import AuthSettings from '$lib/components/admin-settings/AuthSettings.svelte';
import BackupSettings from '$lib/components/admin-settings/BackupSettings.svelte'; import BackupSettings from '$lib/components/admin-settings/BackupSettings.svelte';
import FFmpegSettings from '$lib/components/admin-settings/FFmpegSettings.svelte'; import FFmpegSettings from '$lib/components/admin-settings/FFmpegSettings.svelte';
@ -19,25 +17,22 @@
import ThemeSettings from '$lib/components/admin-settings/ThemeSettings.svelte'; import ThemeSettings from '$lib/components/admin-settings/ThemeSettings.svelte';
import TrashSettings from '$lib/components/admin-settings/TrashSettings.svelte'; import TrashSettings from '$lib/components/admin-settings/TrashSettings.svelte';
import UserSettings from '$lib/components/admin-settings/UserSettings.svelte'; import UserSettings from '$lib/components/admin-settings/UserSettings.svelte';
import HeaderButton from '$lib/components/HeaderButton.svelte';
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte'; import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
import SettingAccordionState from '$lib/components/shared-components/settings/setting-accordion-state.svelte'; import SettingAccordionState from '$lib/components/shared-components/settings/setting-accordion-state.svelte';
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte'; import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
import { QueryParameter } from '$lib/constants'; import { QueryParameter } from '$lib/constants';
import SearchBar from '$lib/elements/SearchBar.svelte'; import SearchBar from '$lib/elements/SearchBar.svelte';
import { downloadManager } from '$lib/managers/download-manager.svelte'; import { getSystemConfigActions } from '$lib/services/system-config.service';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { copyToClipboard } from '$lib/utils'; import { Alert, HStack } from '@immich/ui';
import { downloadBlob } from '$lib/utils/asset-utils';
import { Alert, Button, HStack, Text } from '@immich/ui';
import { import {
mdiAccountOutline, mdiAccountOutline,
mdiBackupRestore, mdiBackupRestore,
mdiBellOutline, mdiBellOutline,
mdiBookshelf, mdiBookshelf,
mdiClockOutline, mdiClockOutline,
mdiContentCopy,
mdiDatabaseOutline, mdiDatabaseOutline,
mdiDownload,
mdiFileDocumentOutline, mdiFileDocumentOutline,
mdiFolderOutline, mdiFolderOutline,
mdiImageOutline, mdiImageOutline,
@ -49,62 +44,20 @@
mdiSync, mdiSync,
mdiTrashCanOutline, mdiTrashCanOutline,
mdiUpdate, mdiUpdate,
mdiUpload,
mdiVideoOutline, mdiVideoOutline,
} from '@mdi/js'; } from '@mdi/js';
import type { Component } from 'svelte'; import type { Component } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
interface Props { type Props = {
data: PageData; data: PageData;
}
let { data }: Props = $props();
let config = $state(data.configs);
let adminSettingElement = $state<ReturnType<typeof AdminSettings>>();
type SettingsComponent = Component<SettingsComponentProps>;
// https://stackoverflow.com/questions/16167581/sort-object-properties-and-json-stringify/43636793#43636793
const jsonReplacer = (key: string, value: unknown) =>
value instanceof Object && !Array.isArray(value)
? Object.keys(value)
.sort()
// eslint-disable-next-line unicorn/no-array-reduce
.reduce((sorted: { [key: string]: unknown }, key) => {
sorted[key] = (value as { [key: string]: unknown })[key];
return sorted;
}, {})
: value;
const downloadConfig = () => {
const blob = new Blob([JSON.stringify(config, jsonReplacer, 2)], { type: 'application/json' });
const downloadKey = 'immich-config.json';
downloadManager.add(downloadKey, blob.size);
downloadManager.update(downloadKey, blob.size);
downloadBlob(blob, downloadKey);
setTimeout(() => downloadManager.clear(downloadKey), 5000);
}; };
let inputElement: HTMLInputElement | undefined = $state(); const { data }: Props = $props();
const uploadConfig = (e: Event) => {
const file = (e.target as HTMLInputElement).files?.[0];
if (!file) {
return;
}
const reader = async () => {
const text = await file.text();
const newConfig = JSON.parse(text);
await adminSettingElement?.handleSave(newConfig);
};
reader().catch((error) => console.error('Error handling JSON config upload', error));
};
const settings: Array<{ const settings: Array<{
component: SettingsComponent; component: Component;
title: string; title: string;
subtitle: string; subtitle: string;
key: string; key: string;
@ -246,9 +199,11 @@
return title.toLowerCase().includes(query) || subtitle.toLowerCase().includes(query); return title.toLowerCase().includes(query) || subtitle.toLowerCase().includes(query);
}), }),
); );
</script>
<input bind:this={inputElement} type="file" accept=".json" style="display: none" onchange={uploadConfig} /> const { CopyToClipboard, Upload, Download } = $derived(
getSystemConfigActions($t, $featureFlags, systemConfigManager.value),
);
</script>
<AdminPageLayout title={data.meta.title}> <AdminPageLayout title={data.meta.title}>
{#snippet buttons()} {#snippet buttons()}
@ -256,58 +211,27 @@
<div class="hidden lg:block"> <div class="hidden lg:block">
<SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} /> <SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} />
</div> </div>
<Button <HeaderButton action={CopyToClipboard} />
leadingIcon={mdiContentCopy} <HeaderButton action={Download} />
onclick={() => copyToClipboard(JSON.stringify(config, jsonReplacer, 2))} <HeaderButton action={Upload} />
size="small"
variant="ghost"
color="secondary"
>
<Text class="hidden md:block">{$t('copy_to_clipboard')}</Text>
</Button>
<Button leadingIcon={mdiDownload} onclick={() => downloadConfig()} size="small" variant="ghost" color="secondary">
<Text class="hidden md:block">{$t('export_as_json')}</Text>
</Button>
{#if !$featureFlags.configFile}
<Button
leadingIcon={mdiUpload}
onclick={() => inputElement?.click()}
size="small"
variant="ghost"
color="secondary"
>
<Text class="hidden md:block">{$t('import_from_json')}</Text>
</Button>
{/if}
</HStack> </HStack>
{/snippet} {/snippet}
<AdminSettings bind:config bind:this={adminSettingElement}> <section id="setting-content" class="flex place-content-center sm:mx-4">
{#snippet children({ savedConfig, defaultConfig })} <section class="w-full pb-28 sm:w-5/6 md:w-4xl">
<section id="setting-content" class="flex place-content-center sm:mx-4"> {#if $featureFlags.configFile}
<section class="w-full pb-28 sm:w-5/6 md:w-4xl"> <Alert color="warning" class="text-dark my-4" title={$t('admin.config_set_by_file')} />
{#if $featureFlags.configFile} {/if}
<Alert color="warning" class="text-dark my-4" title={$t('admin.config_set_by_file')} /> <div class="block lg:hidden">
{/if} <SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} />
<div class="block lg:hidden"> </div>
<SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} /> <SettingAccordionState queryParam={QueryParameter.IS_OPEN}>
</div> {#each filteredSettings as { component: Component, title, subtitle, key, icon } (key)}
<SettingAccordionState queryParam={QueryParameter.IS_OPEN}> <SettingAccordion {title} {subtitle} {key} {icon}>
{#each filteredSettings as { component: Component, title, subtitle, key, icon } (key)} <Component />
<SettingAccordion {title} {subtitle} {key} {icon}> </SettingAccordion>
<Component {/each}
onSave={(config) => adminSettingElement?.handleSave(config)} </SettingAccordionState>
onReset={(options) => adminSettingElement?.handleReset(options)} </section>
disabled={$featureFlags.configFile} </section>
bind:config
{defaultConfig}
{savedConfig}
/>
</SettingAccordion>
{/each}
</SettingAccordionState>
</section>
</section>
{/snippet}
</AdminSettings>
</AdminPageLayout> </AdminPageLayout>

View File

@ -1,15 +1,17 @@
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 { getConfig } from '@immich/sdk'; import { getConfig, getConfigDefaults } from '@immich/sdk';
import type { PageLoad } from './$types'; import type { PageLoad } from './$types';
export const load = (async ({ url }) => { export const load = (async ({ url }) => {
await authenticate(url, { admin: true }); await authenticate(url, { admin: true });
const configs = await getConfig(); const config = await getConfig();
const defaultConfig = await getConfigDefaults();
const $t = await getFormatter(); const $t = await getFormatter();
return { return {
configs, config,
defaultConfig,
meta: { meta: {
title: $t('admin.system_settings'), title: $t('admin.system_settings'),
}, },

View File

@ -3,7 +3,7 @@
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte'; import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { eventManager } from '$lib/managers/event-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte';
import { featureFlags, serverConfig } from '$lib/stores/server-config.store'; import { featureFlags, serverConfig } from '$lib/stores/system-config-manager.svelte';
import { oauth } from '$lib/utils'; import { oauth } from '$lib/utils';
import { getServerErrorMessage, handleError } from '$lib/utils/handle-error'; import { getServerErrorMessage, handleError } from '$lib/utils/handle-error';
import { login, type LoginResponseDto } from '@immich/sdk'; import { login, type LoginResponseDto } from '@immich/sdk';

View File

@ -1,5 +1,5 @@
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { serverConfig } from '$lib/stores/server-config.store'; import { serverConfig } from '$lib/stores/system-config-manager.svelte';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';

View File

@ -12,7 +12,7 @@
import OnboardingUserPrivacy from '$lib/components/onboarding-page/onboarding-user-privacy.svelte'; import OnboardingUserPrivacy from '$lib/components/onboarding-page/onboarding-user-privacy.svelte';
import { AppRoute, QueryParameter } from '$lib/constants'; import { AppRoute, QueryParameter } from '$lib/constants';
import { OnboardingRole } from '$lib/models/onboarding-role'; import { OnboardingRole } from '$lib/models/onboarding-role';
import { retrieveServerConfig, retrieveSystemConfig, serverConfig } from '$lib/stores/server-config.store'; import { retrieveServerConfig, serverConfig, systemConfigManager } from '$lib/stores/system-config-manager.svelte';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { setUserOnboarding, updateAdminOnboarding } from '@immich/sdk'; import { setUserOnboarding, updateAdminOnboarding } from '@immich/sdk';
import { import {
@ -152,11 +152,13 @@
); );
}; };
onMount(async () => {
await retrieveSystemConfig();
});
const OnboardingStep = $derived(onboardingSteps[index].component); const OnboardingStep = $derived(onboardingSteps[index].component);
onMount(async () => {
if (userRole === OnboardingRole.SERVER) {
await systemConfigManager.init();
}
});
</script> </script>
<section id="onboarding-page" class="min-w-dvw flex min-h-dvh p-4"> <section id="onboarding-page" class="min-w-dvw flex min-h-dvh p-4">

View File

@ -2,7 +2,7 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte'; import AuthPageLayout from '$lib/components/layouts/AuthPageLayout.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { retrieveServerConfig } from '$lib/stores/server-config.store'; import { retrieveServerConfig } from '$lib/stores/system-config-manager.svelte';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { signUpAdmin } from '@immich/sdk'; import { signUpAdmin } from '@immich/sdk';
import { Alert, Button, Field, Input, PasswordInput, Text } from '@immich/ui'; import { Alert, Button, Field, Input, PasswordInput, Text } from '@immich/ui';

View File

@ -1,5 +1,5 @@
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { serverConfig } from '$lib/stores/server-config.store'; import { serverConfig } from '$lib/stores/system-config-manager.svelte';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
import { redirect } from '@sveltejs/kit'; import { redirect } from '@sveltejs/kit';
import { get } from 'svelte/store'; import { get } from 'svelte/store';