mirror of
https://github.com/immich-app/immich.git
synced 2025-12-06 09:13:13 +03:00
feat: system settings
This commit is contained in:
30
web/src/lib/components/SystemSettingsCard.svelte
Normal file
30
web/src/lib/components/SystemSettingsCard.svelte
Normal file
@@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
import { Card, CardBody, CardHeader, Heading, Text } from '@immich/ui';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
subtitle?: string | Snippet;
|
||||
children?: Snippet;
|
||||
};
|
||||
|
||||
const { title, subtitle, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Card class="dark:border-light-300" color="secondary">
|
||||
<CardHeader class="dark:border-light-300 px-5 pt-5">
|
||||
<Heading size="small" color="secondary" fontWeight="bold">{title}</Heading>
|
||||
<Text size="small" color="muted">
|
||||
{#if typeof subtitle === 'string'}
|
||||
{subtitle}
|
||||
{:else}
|
||||
{@render subtitle?.()}
|
||||
{/if}
|
||||
</Text>
|
||||
</CardHeader>
|
||||
<CardBody class="dark:border-light-300 px-5 pb-5">
|
||||
<div class="flex flex-col gap-5">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
@@ -1,291 +0,0 @@
|
||||
<script lang="ts">
|
||||
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import FormatMessage from '$lib/elements/FormatMessage.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import AuthDisableLoginConfirmModal from '$lib/modals/AuthDisableLoginConfirmModal.svelte';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { OAuthTokenEndpointAuthMethod, unlinkAllOAuthAccountsAdmin } from '@immich/sdk';
|
||||
import { Button, modalManager, Text, toastManager } from '@immich/ui';
|
||||
import { mdiRestart } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||
const config = $derived(systemConfigManager.value);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
|
||||
const handleToggleOverride = () => {
|
||||
// click runs before bind
|
||||
const previouslyEnabled = configToEdit.oauth.mobileOverrideEnabled;
|
||||
if (!previouslyEnabled && !configToEdit.oauth.mobileRedirectUri) {
|
||||
configToEdit.oauth.mobileRedirectUri = globalThis.location.origin + '/api/oauth/mobile-redirect';
|
||||
}
|
||||
};
|
||||
|
||||
const onBeforeSave = async () => {
|
||||
const allMethodsDisabled = !configToEdit.oauth.enabled && !configToEdit.passwordLogin.enabled;
|
||||
|
||||
if (allMethodsDisabled) {
|
||||
const isConfirmed = await modalManager.show(AuthDisableLoginConfirmModal);
|
||||
if (!isConfirmed) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleUnlinkAllOAuthAccounts = async () => {
|
||||
const confirmed = await modalManager.showDialog({
|
||||
icon: mdiRestart,
|
||||
title: $t('admin.unlink_all_oauth_accounts'),
|
||||
prompt: $t('admin.unlink_all_oauth_accounts_prompt'),
|
||||
confirmColor: 'danger',
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await unlinkAllOAuthAccountsAdmin();
|
||||
toastManager.success();
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.something_went_wrong'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" onsubmit={(e) => e.preventDefault()}>
|
||||
<div class="ms-4 mt-4 flex flex-col">
|
||||
<SettingAccordion
|
||||
key="oauth"
|
||||
title={$t('admin.oauth_settings')}
|
||||
subtitle={$t('admin.oauth_settings_description')}
|
||||
>
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<Text size="small">
|
||||
<FormatMessage key="admin.oauth_settings_more_details">
|
||||
{#snippet children({ message })}
|
||||
<a
|
||||
href="https://docs.immich.app/administration/oauth"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{message}
|
||||
</a>
|
||||
{/snippet}
|
||||
</FormatMessage>
|
||||
</Text>
|
||||
|
||||
<SettingSwitch
|
||||
{disabled}
|
||||
title={$t('admin.oauth_enable_description')}
|
||||
bind:checked={configToEdit.oauth.enabled}
|
||||
/>
|
||||
|
||||
{#if configToEdit.oauth.enabled}
|
||||
<hr />
|
||||
|
||||
<div class="flex items-center gap-2 justify-between">
|
||||
<Text size="small">{$t('admin.unlink_all_oauth_accounts_description')}</Text>
|
||||
<Button size="small" onclick={handleUnlinkAllOAuthAccounts}
|
||||
>{$t('admin.unlink_all_oauth_accounts')}</Button
|
||||
>
|
||||
</div>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="ISSUER_URL"
|
||||
bind:value={configToEdit.oauth.issuerUrl}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.issuerUrl === config.oauth.issuerUrl)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="CLIENT_ID"
|
||||
bind:value={configToEdit.oauth.clientId}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.clientId === config.oauth.clientId)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="CLIENT_SECRET"
|
||||
description={$t('admin.oauth_client_secret_description')}
|
||||
bind:value={configToEdit.oauth.clientSecret}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.clientSecret === config.oauth.clientSecret)}
|
||||
/>
|
||||
|
||||
{#if configToEdit.oauth.clientSecret}
|
||||
<SettingSelect
|
||||
label="TOKEN_ENDPOINT_AUTH_METHOD"
|
||||
bind:value={configToEdit.oauth.tokenEndpointAuthMethod}
|
||||
disabled={disabled || !configToEdit.oauth.enabled || !configToEdit.oauth.clientSecret}
|
||||
isEdited={!(configToEdit.oauth.tokenEndpointAuthMethod === config.oauth.tokenEndpointAuthMethod)}
|
||||
options={[
|
||||
{ value: OAuthTokenEndpointAuthMethod.ClientSecretPost, text: 'client_secret_post' },
|
||||
{ value: OAuthTokenEndpointAuthMethod.ClientSecretBasic, text: 'client_secret_basic' },
|
||||
]}
|
||||
name="tokenEndpointAuthMethod"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="SCOPE"
|
||||
bind:value={configToEdit.oauth.scope}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.scope === config.oauth.scope)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="ID_TOKEN_SIGNED_RESPONSE_ALG"
|
||||
bind:value={configToEdit.oauth.signingAlgorithm}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.signingAlgorithm === config.oauth.signingAlgorithm)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="USERINFO_SIGNED_RESPONSE_ALG"
|
||||
bind:value={configToEdit.oauth.profileSigningAlgorithm}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.profileSigningAlgorithm === config.oauth.profileSigningAlgorithm)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.oauth_timeout')}
|
||||
description={$t('admin.oauth_timeout_description')}
|
||||
required={true}
|
||||
bind:value={configToEdit.oauth.timeout}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.timeout === config.oauth.timeout)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.oauth_storage_label_claim')}
|
||||
description={$t('admin.oauth_storage_label_claim_description')}
|
||||
bind:value={configToEdit.oauth.storageLabelClaim}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.storageLabelClaim === config.oauth.storageLabelClaim)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.oauth_role_claim')}
|
||||
description={$t('admin.oauth_role_claim_description')}
|
||||
bind:value={configToEdit.oauth.roleClaim}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.roleClaim === config.oauth.roleClaim)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.oauth_storage_quota_claim')}
|
||||
description={$t('admin.oauth_storage_quota_claim_description')}
|
||||
bind:value={configToEdit.oauth.storageQuotaClaim}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.storageQuotaClaim === config.oauth.storageQuotaClaim)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.oauth_storage_quota_default')}
|
||||
description={$t('admin.oauth_storage_quota_default_description')}
|
||||
bind:value={configToEdit.oauth.defaultStorageQuota}
|
||||
required={false}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.defaultStorageQuota === config.oauth.defaultStorageQuota)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.oauth_button_text')}
|
||||
bind:value={configToEdit.oauth.buttonText}
|
||||
required={false}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.buttonText === config.oauth.buttonText)}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title={$t('admin.oauth_auto_register')}
|
||||
subtitle={$t('admin.oauth_auto_register_description')}
|
||||
bind:checked={configToEdit.oauth.autoRegister}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title={$t('admin.oauth_auto_launch')}
|
||||
subtitle={$t('admin.oauth_auto_launch_description')}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
bind:checked={configToEdit.oauth.autoLaunch}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title={$t('admin.oauth_mobile_redirect_uri_override')}
|
||||
subtitle={$t('admin.oauth_mobile_redirect_uri_override_description', {
|
||||
values: { callback: 'app.immich:///oauth-callback' },
|
||||
})}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
onToggle={() => handleToggleOverride()}
|
||||
bind:checked={configToEdit.oauth.mobileOverrideEnabled}
|
||||
/>
|
||||
|
||||
{#if configToEdit.oauth.mobileOverrideEnabled}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.oauth_mobile_redirect_uri')}
|
||||
bind:value={configToEdit.oauth.mobileRedirectUri}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.mobileRedirectUri === config.oauth.mobileRedirectUri)}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
key="password"
|
||||
title={$t('admin.password_settings')}
|
||||
subtitle={$t('admin.password_settings_description')}
|
||||
>
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ms-4 mt-4 flex flex-col">
|
||||
<SettingSwitch
|
||||
title={$t('admin.password_enable_description')}
|
||||
{disabled}
|
||||
bind:checked={configToEdit.passwordLogin.enabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingButtonsRow bind:configToEdit keys={['passwordLogin', 'oauth']} {onBeforeSave} {disabled} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,83 +0,0 @@
|
||||
<script lang="ts">
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import FormatMessage from '$lib/elements/FormatMessage.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||
const config = $derived(systemConfigManager.value);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
|
||||
let cronExpressionOptions = $derived([
|
||||
{ text: $t('interval.night_at_midnight'), value: '0 0 * * *' },
|
||||
{ text: $t('interval.night_at_twoam'), value: '0 02 * * *' },
|
||||
{ text: $t('interval.day_at_onepm'), value: '0 13 * * *' },
|
||||
{ text: $t('interval.hours', { values: { hours: 6 } }), value: '0 */6 * * *' },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.backup_database_enable_description')}
|
||||
{disabled}
|
||||
bind:checked={configToEdit.backup.database.enabled}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
options={cronExpressionOptions}
|
||||
disabled={disabled || !configToEdit.backup.database.enabled}
|
||||
name="expression"
|
||||
label={$t('admin.cron_expression_presets')}
|
||||
bind:value={configToEdit.backup.database.cronExpression}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.backup.database.enabled}
|
||||
label={$t('admin.cron_expression')}
|
||||
bind:value={configToEdit.backup.database.cronExpression}
|
||||
isEdited={configToEdit.backup.database.cronExpression !== config.backup.database.cronExpression}
|
||||
>
|
||||
{#snippet descriptionSnippet()}
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
<FormatMessage key="admin.cron_expression_description">
|
||||
{#snippet children({ message })}
|
||||
<a
|
||||
href="https://crontab.guru/#{configToEdit.backup.database.cronExpression.replaceAll(' ', '_')}"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{message}
|
||||
<br />
|
||||
</a>
|
||||
{/snippet}
|
||||
</FormatMessage>
|
||||
</p>
|
||||
{/snippet}
|
||||
</SettingInputField>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
required={true}
|
||||
label={$t('admin.backup_keep_last_amount')}
|
||||
disabled={disabled || !configToEdit.backup.database.enabled}
|
||||
bind:value={configToEdit.backup.database.keepLastAmount}
|
||||
isEdited={configToEdit.backup.database.keepLastAmount !== config.backup.database.keepLastAmount}
|
||||
/>
|
||||
|
||||
<SettingButtonsRow {disabled} bind:configToEdit keys={['backup']} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,180 +0,0 @@
|
||||
<script lang="ts">
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import { Colorspace, ImageFormat } from '@immich/sdk';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||
const config = $derived(systemConfigManager.value);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
|
||||
<div class="ms-4 mt-4">
|
||||
<SettingAccordion
|
||||
key="thumbnail-settings"
|
||||
title={$t('admin.image_thumbnail_title')}
|
||||
subtitle={$t('admin.image_thumbnail_description')}
|
||||
>
|
||||
<SettingSelect
|
||||
label={$t('admin.image_format')}
|
||||
desc={$t('admin.image_format_description')}
|
||||
bind:value={configToEdit.image.thumbnail.format}
|
||||
options={[
|
||||
{ value: ImageFormat.Jpeg, text: 'JPEG' },
|
||||
{ value: ImageFormat.Webp, text: 'WebP' },
|
||||
]}
|
||||
name="format"
|
||||
isEdited={configToEdit.image.thumbnail.format !== config.image.thumbnail.format}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label={$t('admin.image_resolution')}
|
||||
desc={$t('admin.image_resolution_description')}
|
||||
number
|
||||
bind:value={configToEdit.image.thumbnail.size}
|
||||
options={[
|
||||
{ value: 1080, text: '1080p' },
|
||||
{ value: 720, text: '720p' },
|
||||
{ value: 480, text: '480p' },
|
||||
{ value: 250, text: '250p' },
|
||||
{ value: 200, text: '200p' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={configToEdit.image.thumbnail.size !== config.image.thumbnail.size}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.image_quality')}
|
||||
description={$t('admin.image_thumbnail_quality_description')}
|
||||
bind:value={configToEdit.image.thumbnail.quality}
|
||||
isEdited={configToEdit.image.thumbnail.quality !== config.image.thumbnail.quality}
|
||||
{disabled}
|
||||
/>
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
key="preview-settings"
|
||||
title={$t('admin.image_preview_title')}
|
||||
subtitle={$t('admin.image_preview_description')}
|
||||
>
|
||||
<SettingSelect
|
||||
label={$t('admin.image_format')}
|
||||
desc={$t('admin.image_format_description')}
|
||||
bind:value={configToEdit.image.preview.format}
|
||||
options={[
|
||||
{ value: ImageFormat.Jpeg, text: 'JPEG' },
|
||||
{ value: ImageFormat.Webp, text: 'WebP' },
|
||||
]}
|
||||
name="format"
|
||||
isEdited={configToEdit.image.preview.format !== config.image.preview.format}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label={$t('admin.image_resolution')}
|
||||
desc={$t('admin.image_resolution_description')}
|
||||
number
|
||||
bind:value={configToEdit.image.preview.size}
|
||||
options={[
|
||||
{ value: 2160, text: '4K' },
|
||||
{ value: 1440, text: '1440p' },
|
||||
{ value: 1080, text: '1080p' },
|
||||
{ value: 720, text: '720p' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={configToEdit.image.preview.size !== config.image.preview.size}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.image_quality')}
|
||||
description={$t('admin.image_preview_quality_description')}
|
||||
bind:value={configToEdit.image.preview.quality}
|
||||
isEdited={configToEdit.image.preview.quality !== config.image.preview.quality}
|
||||
{disabled}
|
||||
/>
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
key="fullsize-settings"
|
||||
title={$t('admin.image_fullsize_title')}
|
||||
subtitle={$t('admin.image_fullsize_description')}
|
||||
>
|
||||
<SettingSwitch
|
||||
title={$t('admin.image_fullsize_enabled')}
|
||||
subtitle={$t('admin.image_fullsize_enabled_description')}
|
||||
checked={configToEdit.image.fullsize.enabled}
|
||||
onToggle={(isChecked) => (configToEdit.image.fullsize.enabled = isChecked)}
|
||||
isEdited={configToEdit.image.fullsize.enabled !== config.image.fullsize.enabled}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
<SettingSelect
|
||||
label={$t('admin.image_format')}
|
||||
desc={$t('admin.image_format_description')}
|
||||
bind:value={configToEdit.image.fullsize.format}
|
||||
options={[
|
||||
{ value: ImageFormat.Jpeg, text: 'JPEG' },
|
||||
{ value: ImageFormat.Webp, text: 'WebP' },
|
||||
]}
|
||||
name="format"
|
||||
isEdited={configToEdit.image.fullsize.format !== config.image.fullsize.format}
|
||||
disabled={disabled || !configToEdit.image.fullsize.enabled}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.image_quality')}
|
||||
description={$t('admin.image_fullsize_quality_description')}
|
||||
bind:value={configToEdit.image.fullsize.quality}
|
||||
isEdited={configToEdit.image.fullsize.quality !== config.image.fullsize.quality}
|
||||
disabled={disabled || !configToEdit.image.fullsize.enabled}
|
||||
/>
|
||||
</SettingAccordion>
|
||||
|
||||
<div class="mt-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.image_prefer_wide_gamut')}
|
||||
subtitle={$t('admin.image_prefer_wide_gamut_setting_description')}
|
||||
checked={configToEdit.image.colorspace === Colorspace.P3}
|
||||
onToggle={(isChecked) => (configToEdit.image.colorspace = isChecked ? Colorspace.P3 : Colorspace.Srgb)}
|
||||
isEdited={configToEdit.image.colorspace !== config.image.colorspace}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.image_prefer_embedded_preview')}
|
||||
subtitle={$t('admin.image_prefer_embedded_preview_setting_description')}
|
||||
checked={configToEdit.image.extractEmbedded}
|
||||
onToggle={() => (configToEdit.image.extractEmbedded = !configToEdit.image.extractEmbedded)}
|
||||
isEdited={configToEdit.image.extractEmbedded !== config.image.extractEmbedded}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ms-4 mt-4">
|
||||
<SettingButtonsRow bind:configToEdit keys={['image']} {disabled} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,68 +0,0 @@
|
||||
<script lang="ts">
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { getQueueName } from '$lib/utils';
|
||||
import { QueueName, type SystemConfigJobDto } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||
const config = $derived(systemConfigManager.value);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
|
||||
const queueNames = [
|
||||
QueueName.ThumbnailGeneration,
|
||||
QueueName.MetadataExtraction,
|
||||
QueueName.Library,
|
||||
QueueName.Sidecar,
|
||||
QueueName.SmartSearch,
|
||||
QueueName.FaceDetection,
|
||||
QueueName.FacialRecognition,
|
||||
QueueName.VideoConversion,
|
||||
QueueName.StorageTemplateMigration,
|
||||
QueueName.Migration,
|
||||
QueueName.Ocr,
|
||||
];
|
||||
|
||||
function isSystemConfigJobDto(jobName: string): jobName is keyof SystemConfigJobDto {
|
||||
return jobName in configToEdit.job;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
|
||||
{#each queueNames as queueName (queueName)}
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
{#if isSystemConfigJobDto(queueName)}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
{disabled}
|
||||
label={$t('admin.job_concurrency', { values: { job: $getQueueName(queueName) } })}
|
||||
description=""
|
||||
bind:value={configToEdit.job[queueName].concurrency}
|
||||
required={true}
|
||||
isEdited={!(configToEdit.job[queueName].concurrency == config.job[queueName].concurrency)}
|
||||
/>
|
||||
{:else}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.job_concurrency', { values: { job: $getQueueName(queueName) } })}
|
||||
description=""
|
||||
value={1}
|
||||
disabled={true}
|
||||
title={$t('admin.job_not_concurrency_safe')}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="ms-4">
|
||||
<SettingButtonsRow bind:configToEdit keys={['job']} {disabled} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,46 +0,0 @@
|
||||
<script lang="ts">
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { LogLevel } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||
const config = $derived(systemConfigManager.value);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.logging_enable_description')}
|
||||
{disabled}
|
||||
bind:checked={configToEdit.logging.enabled}
|
||||
/>
|
||||
<SettingSelect
|
||||
label={$t('level')}
|
||||
desc={$t('admin.logging_level_description')}
|
||||
bind:value={configToEdit.logging.level}
|
||||
options={[
|
||||
{ value: LogLevel.Fatal, text: 'Fatal' },
|
||||
{ value: LogLevel.Error, text: 'Error' },
|
||||
{ value: LogLevel.Warn, text: 'Warn' },
|
||||
{ value: LogLevel.Log, text: 'Log' },
|
||||
{ value: LogLevel.Debug, text: 'Debug' },
|
||||
{ value: LogLevel.Verbose, text: 'Verbose' },
|
||||
]}
|
||||
name="level"
|
||||
isEdited={configToEdit.logging.level !== config.logging.level}
|
||||
disabled={disabled || !configToEdit.logging.enabled}
|
||||
/>
|
||||
|
||||
<SettingButtonsRow bind:configToEdit keys={['logging']} {disabled} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,331 +0,0 @@
|
||||
<script lang="ts">
|
||||
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import FormatMessage from '$lib/elements/FormatMessage.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { Button, IconButton } from '@immich/ui';
|
||||
import { mdiPlus, mdiTrashCanOutline } from '@mdi/js';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||
const config = $derived(systemConfigManager.value);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
</script>
|
||||
|
||||
<div class="mt-2">
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" class="mx-4 mt-4" onsubmit={(event) => event.preventDefault()}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.machine_learning_enabled')}
|
||||
subtitle={$t('admin.machine_learning_enabled_description')}
|
||||
{disabled}
|
||||
bind:checked={configToEdit.machineLearning.enabled}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
{#each configToEdit.machineLearning.urls as _, i (i)}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={i === 0 ? $t('url') : undefined}
|
||||
description={i === 0 ? $t('admin.machine_learning_url_description') : undefined}
|
||||
bind:value={configToEdit.machineLearning.urls[i]}
|
||||
required={i === 0}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled}
|
||||
isEdited={i === 0 && !isEqual(configToEdit.machineLearning.urls, config.machineLearning.urls)}
|
||||
>
|
||||
{#snippet trailingSnippet()}
|
||||
{#if configToEdit.machineLearning.urls.length > 1}
|
||||
<IconButton
|
||||
aria-label=""
|
||||
onclick={() => configToEdit.machineLearning.urls.splice(i, 1)}
|
||||
icon={mdiTrashCanOutline}
|
||||
color="danger"
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</SettingInputField>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
class="mb-2"
|
||||
size="small"
|
||||
shape="round"
|
||||
leadingIcon={mdiPlus}
|
||||
onclick={() => configToEdit.machineLearning.urls.push('')}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled}>{$t('add_url')}</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SettingAccordion
|
||||
key="availability-checks"
|
||||
title={$t('admin.machine_learning_availability_checks')}
|
||||
subtitle={$t('admin.machine_learning_availability_checks_description')}
|
||||
>
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.machine_learning_availability_checks_enabled')}
|
||||
bind:checked={configToEdit.machineLearning.availabilityChecks.enabled}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_availability_checks_interval')}
|
||||
bind:value={configToEdit.machineLearning.availabilityChecks.interval}
|
||||
description={$t('admin.machine_learning_availability_checks_interval_description')}
|
||||
disabled={disabled ||
|
||||
!configToEdit.machineLearning.enabled ||
|
||||
!configToEdit.machineLearning.availabilityChecks.enabled}
|
||||
isEdited={configToEdit.machineLearning.availabilityChecks.interval !==
|
||||
config.machineLearning.availabilityChecks.interval}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_availability_checks_timeout')}
|
||||
bind:value={configToEdit.machineLearning.availabilityChecks.timeout}
|
||||
description={$t('admin.machine_learning_availability_checks_timeout_description')}
|
||||
disabled={disabled ||
|
||||
!configToEdit.machineLearning.enabled ||
|
||||
!configToEdit.machineLearning.availabilityChecks.enabled}
|
||||
isEdited={configToEdit.machineLearning.availabilityChecks.timeout !==
|
||||
config.machineLearning.availabilityChecks.timeout}
|
||||
/>
|
||||
</div>
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
key="smart-search"
|
||||
title={$t('admin.machine_learning_smart_search')}
|
||||
subtitle={$t('admin.machine_learning_smart_search_description')}
|
||||
>
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.machine_learning_smart_search_enabled')}
|
||||
subtitle={$t('admin.machine_learning_smart_search_enabled_description')}
|
||||
bind:checked={configToEdit.machineLearning.clip.enabled}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.machine_learning_clip_model')}
|
||||
bind:value={configToEdit.machineLearning.clip.modelName}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.clip.enabled}
|
||||
isEdited={configToEdit.machineLearning.clip.modelName !== config.machineLearning.clip.modelName}
|
||||
>
|
||||
{#snippet descriptionSnippet()}
|
||||
<p class="immich-form-label pb-2 text-sm">
|
||||
<FormatMessage key="admin.machine_learning_clip_model_description">
|
||||
{#snippet children({ message })}
|
||||
<a target="_blank" href="https://huggingface.co/immich-app"><u>{message}</u></a>
|
||||
{/snippet}
|
||||
</FormatMessage>
|
||||
</p>
|
||||
{/snippet}
|
||||
</SettingInputField>
|
||||
</div>
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
key="duplicate-detection"
|
||||
title={$t('admin.machine_learning_duplicate_detection')}
|
||||
subtitle={$t('admin.machine_learning_duplicate_detection_setting_description')}
|
||||
>
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.machine_learning_duplicate_detection_enabled')}
|
||||
subtitle={$t('admin.machine_learning_duplicate_detection_enabled_description')}
|
||||
bind:checked={configToEdit.machineLearning.duplicateDetection.enabled}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.clip.enabled}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_max_detection_distance')}
|
||||
bind:value={configToEdit.machineLearning.duplicateDetection.maxDistance}
|
||||
step="0.0005"
|
||||
min={0.001}
|
||||
max={0.1}
|
||||
description={$t('admin.machine_learning_max_detection_distance_description')}
|
||||
disabled={disabled || !featureFlagsManager.value.duplicateDetection}
|
||||
isEdited={configToEdit.machineLearning.duplicateDetection.maxDistance !==
|
||||
config.machineLearning.duplicateDetection.maxDistance}
|
||||
/>
|
||||
</div>
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
key="facial-recognition"
|
||||
title={$t('admin.machine_learning_facial_recognition')}
|
||||
subtitle={$t('admin.machine_learning_facial_recognition_description')}
|
||||
>
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.machine_learning_facial_recognition_setting')}
|
||||
subtitle={$t('admin.machine_learning_facial_recognition_setting_description')}
|
||||
bind:checked={configToEdit.machineLearning.facialRecognition.enabled}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<SettingSelect
|
||||
label={$t('admin.machine_learning_facial_recognition_model')}
|
||||
desc={$t('admin.machine_learning_facial_recognition_model_description')}
|
||||
name="facial-recognition-model"
|
||||
bind:value={configToEdit.machineLearning.facialRecognition.modelName}
|
||||
options={[
|
||||
{ value: 'antelopev2', text: 'antelopev2' },
|
||||
{ value: 'buffalo_l', text: 'buffalo_l' },
|
||||
{ value: 'buffalo_m', text: 'buffalo_m' },
|
||||
{ value: 'buffalo_s', text: 'buffalo_s' },
|
||||
]}
|
||||
disabled={disabled ||
|
||||
!configToEdit.machineLearning.enabled ||
|
||||
!configToEdit.machineLearning.facialRecognition.enabled}
|
||||
isEdited={configToEdit.machineLearning.facialRecognition.modelName !==
|
||||
config.machineLearning.facialRecognition.modelName}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_min_detection_score')}
|
||||
description={$t('admin.machine_learning_min_detection_score_description')}
|
||||
bind:value={configToEdit.machineLearning.facialRecognition.minScore}
|
||||
step="0.01"
|
||||
min={0.1}
|
||||
max={1}
|
||||
disabled={disabled ||
|
||||
!configToEdit.machineLearning.enabled ||
|
||||
!configToEdit.machineLearning.facialRecognition.enabled}
|
||||
isEdited={configToEdit.machineLearning.facialRecognition.minScore !==
|
||||
config.machineLearning.facialRecognition.minScore}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_max_recognition_distance')}
|
||||
description={$t('admin.machine_learning_max_recognition_distance_description')}
|
||||
bind:value={configToEdit.machineLearning.facialRecognition.maxDistance}
|
||||
step="0.01"
|
||||
min={0.1}
|
||||
max={2}
|
||||
disabled={disabled ||
|
||||
!configToEdit.machineLearning.enabled ||
|
||||
!configToEdit.machineLearning.facialRecognition.enabled}
|
||||
isEdited={configToEdit.machineLearning.facialRecognition.maxDistance !==
|
||||
config.machineLearning.facialRecognition.maxDistance}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_min_recognized_faces')}
|
||||
description={$t('admin.machine_learning_min_recognized_faces_description')}
|
||||
bind:value={configToEdit.machineLearning.facialRecognition.minFaces}
|
||||
step="1"
|
||||
min={1}
|
||||
disabled={disabled ||
|
||||
!configToEdit.machineLearning.enabled ||
|
||||
!configToEdit.machineLearning.facialRecognition.enabled}
|
||||
isEdited={configToEdit.machineLearning.facialRecognition.minFaces !==
|
||||
config.machineLearning.facialRecognition.minFaces}
|
||||
/>
|
||||
</div>
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
key="ocr"
|
||||
title={$t('admin.machine_learning_ocr')}
|
||||
subtitle={$t('admin.machine_learning_ocr_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.machine_learning_ocr_enabled')}
|
||||
subtitle={$t('admin.machine_learning_ocr_enabled_description')}
|
||||
bind:checked={configToEdit.machineLearning.ocr.enabled}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<SettingSelect
|
||||
label={$t('admin.machine_learning_ocr_model')}
|
||||
desc={$t('admin.machine_learning_ocr_model_description')}
|
||||
name="ocr-model"
|
||||
bind:value={configToEdit.machineLearning.ocr.modelName}
|
||||
options={[
|
||||
{ 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 (English-only)', value: 'EN__PP-OCRv5_mobile' },
|
||||
{ text: 'PP-OCRv5_mobile (Greek and English)', value: 'EL__PP-OCRv5_mobile' },
|
||||
{ text: 'PP-OCRv5_mobile (Korean and English)', value: 'KOREAN__PP-OCRv5_mobile' },
|
||||
{ text: 'PP-OCRv5_mobile (Latin script languages)', value: 'LATIN__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' },
|
||||
]}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.ocr.enabled}
|
||||
isEdited={configToEdit.machineLearning.ocr.modelName !== config.machineLearning.ocr.modelName}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_ocr_min_detection_score')}
|
||||
description={$t('admin.machine_learning_ocr_min_detection_score_description')}
|
||||
bind:value={configToEdit.machineLearning.ocr.minDetectionScore}
|
||||
step="0.1"
|
||||
min={0.1}
|
||||
max={1}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.ocr.enabled}
|
||||
isEdited={configToEdit.machineLearning.ocr.minDetectionScore !==
|
||||
config.machineLearning.ocr.minDetectionScore}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_ocr_min_recognition_score')}
|
||||
description={$t('admin.machine_learning_ocr_min_score_recognition_description')}
|
||||
bind:value={configToEdit.machineLearning.ocr.minRecognitionScore}
|
||||
step="0.1"
|
||||
min={0.1}
|
||||
max={1}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.ocr.enabled}
|
||||
isEdited={configToEdit.machineLearning.ocr.minRecognitionScore !==
|
||||
config.machineLearning.ocr.minRecognitionScore}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_ocr_max_resolution')}
|
||||
description={$t('admin.machine_learning_ocr_max_resolution_description')}
|
||||
bind:value={configToEdit.machineLearning.ocr.maxResolution}
|
||||
min={1}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.ocr.enabled}
|
||||
isEdited={configToEdit.machineLearning.ocr.maxResolution !== config.machineLearning.ocr.maxResolution}
|
||||
/>
|
||||
</div>
|
||||
</SettingAccordion>
|
||||
<SettingButtonsRow bind:configToEdit keys={['machineLearning']} {disabled} />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,35 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { MaintenanceAction, setMaintenanceMode } from '@immich/sdk';
|
||||
import { Button } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
let { disabled = false }: Props = $props();
|
||||
|
||||
async function start() {
|
||||
try {
|
||||
await setMaintenanceMode({
|
||||
setMaintenanceModeDto: {
|
||||
action: MaintenanceAction.Start,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(error, $t('admin.maintenance_start_error'));
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<div class="ms-4 mt-4 flex items-end gap-4">
|
||||
<Button shape="round" type="submit" {disabled} size="small" onclick={start}
|
||||
>{$t('admin.maintenance_start')}</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,82 +0,0 @@
|
||||
<script lang="ts">
|
||||
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import FormatMessage from '$lib/elements/FormatMessage.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||
const config = $derived(systemConfigManager.value);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
</script>
|
||||
|
||||
<div class="mt-2">
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
|
||||
<div class="flex flex-col gap-4">
|
||||
<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">
|
||||
<SettingSwitch
|
||||
title={$t('admin.map_enable_description')}
|
||||
subtitle={$t('admin.map_implications')}
|
||||
{disabled}
|
||||
bind:checked={configToEdit.map.enabled}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.map_light_style')}
|
||||
description={$t('admin.map_style_description')}
|
||||
bind:value={configToEdit.map.lightStyle}
|
||||
disabled={disabled || !configToEdit.map.enabled}
|
||||
isEdited={configToEdit.map.lightStyle !== config.map.lightStyle}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.map_dark_style')}
|
||||
description={$t('admin.map_style_description')}
|
||||
bind:value={configToEdit.map.darkStyle}
|
||||
disabled={disabled || !configToEdit.map.enabled}
|
||||
isEdited={configToEdit.map.darkStyle !== config.map.darkStyle}
|
||||
/>
|
||||
</div></SettingAccordion
|
||||
>
|
||||
|
||||
<SettingAccordion key="reverse-geocoding" title={$t('admin.map_reverse_geocoding_settings')}>
|
||||
{#snippet subtitleSnippet()}
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
<FormatMessage key="admin.map_manage_reverse_geocoding_settings">
|
||||
{#snippet children({ message })}
|
||||
<a
|
||||
href="https://docs.immich.app/features/reverse-geocoding"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{message}
|
||||
</a>
|
||||
{/snippet}
|
||||
</FormatMessage>
|
||||
</p>
|
||||
{/snippet}
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.map_reverse_geocoding_enable_description')}
|
||||
{disabled}
|
||||
bind:checked={configToEdit.reverseGeocoding.enabled}
|
||||
/>
|
||||
</div></SettingAccordion
|
||||
>
|
||||
|
||||
<SettingButtonsRow bind:configToEdit keys={['map', 'reverseGeocoding']} {disabled} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,28 +0,0 @@
|
||||
<script lang="ts">
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
</script>
|
||||
|
||||
<div class="mt-2">
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" class="mx-4 mt-4" onsubmit={(event) => event.preventDefault()}>
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.metadata_faces_import_setting')}
|
||||
subtitle={$t('admin.metadata_faces_import_setting_description')}
|
||||
bind:checked={configToEdit.metadata.faces.import}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SettingButtonsRow bind:configToEdit keys={['metadata']} {disabled} />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,27 +0,0 @@
|
||||
<script lang="ts">
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
|
||||
<div class="ms-4 mt-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.version_check_enabled_description')}
|
||||
subtitle={$t('admin.version_check_implications')}
|
||||
bind:checked={configToEdit.newVersionCheck.enabled}
|
||||
{disabled}
|
||||
/>
|
||||
<SettingButtonsRow bind:configToEdit keys={['newVersionCheck']} {disabled} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,64 +0,0 @@
|
||||
<script lang="ts">
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.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 { SettingInputFieldType } from '$lib/constants';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||
const config = $derived(systemConfigManager.value);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
</script>
|
||||
|
||||
<div class="mt-2">
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" class="mx-4 mt-4" onsubmit={(event) => event.preventDefault()}>
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.nightly_tasks_start_time_setting')}
|
||||
description={$t('admin.nightly_tasks_start_time_setting_description')}
|
||||
bind:value={configToEdit.nightlyTasks.startTime}
|
||||
required={true}
|
||||
{disabled}
|
||||
isEdited={!(configToEdit.nightlyTasks.startTime === config.nightlyTasks.startTime)}
|
||||
/>
|
||||
<SettingSwitch
|
||||
title={$t('admin.nightly_tasks_database_cleanup_setting')}
|
||||
subtitle={$t('admin.nightly_tasks_database_cleanup_setting_description')}
|
||||
bind:checked={configToEdit.nightlyTasks.databaseCleanup}
|
||||
{disabled}
|
||||
/>
|
||||
<SettingSwitch
|
||||
title={$t('admin.nightly_tasks_missing_thumbnails_setting')}
|
||||
subtitle={$t('admin.nightly_tasks_missing_thumbnails_setting_description')}
|
||||
bind:checked={configToEdit.nightlyTasks.missingThumbnails}
|
||||
{disabled}
|
||||
/>
|
||||
<SettingSwitch
|
||||
title={$t('admin.nightly_tasks_cluster_new_faces_setting')}
|
||||
subtitle={$t('admin.nightly_tasks_cluster_faces_setting_description')}
|
||||
bind:checked={configToEdit.nightlyTasks.clusterNewFaces}
|
||||
{disabled}
|
||||
/>
|
||||
<SettingSwitch
|
||||
title={$t('admin.nightly_tasks_generate_memories_setting')}
|
||||
subtitle={$t('admin.nightly_tasks_generate_memories_setting_description')}
|
||||
bind:checked={configToEdit.nightlyTasks.generateMemories}
|
||||
{disabled}
|
||||
/>
|
||||
<SettingSwitch
|
||||
title={$t('admin.nightly_tasks_sync_quota_usage_setting')}
|
||||
subtitle={$t('admin.nightly_tasks_sync_quota_usage_setting_description')}
|
||||
bind:checked={configToEdit.nightlyTasks.syncQuotaUsage}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SettingButtonsRow bind:configToEdit keys={['nightlyTasks']} {disabled} />
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,49 +0,0 @@
|
||||
<script lang="ts">
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.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 { SettingInputFieldType } from '$lib/constants';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||
const config = $derived(systemConfigManager.value);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
|
||||
<div class="mt-4 ms-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.server_external_domain_settings')}
|
||||
description={$t('admin.server_external_domain_settings_description')}
|
||||
bind:value={configToEdit.server.externalDomain}
|
||||
isEdited={configToEdit.server.externalDomain !== config.server.externalDomain}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.server_welcome_message')}
|
||||
description={$t('admin.server_welcome_message_description')}
|
||||
bind:value={configToEdit.server.loginPageMessage}
|
||||
isEdited={configToEdit.server.loginPageMessage !== config.server.loginPageMessage}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title={$t('admin.server_public_users')}
|
||||
subtitle={$t('admin.server_public_users_description')}
|
||||
{disabled}
|
||||
bind:checked={configToEdit.server.publicUsers}
|
||||
/>
|
||||
|
||||
<div class="ms-4">
|
||||
<SettingButtonsRow bind:configToEdit keys={['server']} {disabled} />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,30 +0,0 @@
|
||||
<script lang="ts">
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
|
||||
import SettingTextarea from '$lib/components/shared-components/settings/setting-textarea.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||
const config = $derived(systemConfigManager.value);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingTextarea
|
||||
{disabled}
|
||||
label={$t('admin.theme_custom_css_settings')}
|
||||
description={$t('admin.theme_custom_css_settings_description')}
|
||||
bind:value={configToEdit.theme.customCss}
|
||||
isEdited={configToEdit.theme.customCss !== config.theme.customCss}
|
||||
/>
|
||||
|
||||
<SettingButtonsRow bind:configToEdit keys={['theme']} {disabled} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,42 +0,0 @@
|
||||
<script lang="ts">
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.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 { SettingInputFieldType } from '$lib/constants';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||
const config = $derived(systemConfigManager.value);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" onsubmit={(event) => event.preventDefault()}>
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.trash_enabled_description')}
|
||||
{disabled}
|
||||
bind:checked={configToEdit.trash.enabled}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.trash_number_of_days')}
|
||||
description={$t('admin.trash_number_of_days_description')}
|
||||
bind:value={configToEdit.trash.days}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.trash.enabled}
|
||||
isEdited={configToEdit.trash.days !== config.trash.days}
|
||||
/>
|
||||
|
||||
<SettingButtonsRow bind:configToEdit keys={['trash']} {disabled} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,35 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/SystemConfigButtonRow.svelte';
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||
const config = $derived(systemConfigManager.value);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" onsubmit={(e) => e.preventDefault()}>
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
min={1}
|
||||
label={$t('admin.user_delete_delay_settings')}
|
||||
description={$t('admin.user_delete_delay_settings_description')}
|
||||
bind:value={configToEdit.user.deleteDelay}
|
||||
isEdited={configToEdit.user.deleteDelay !== config.user.deleteDelay}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ms-4">
|
||||
<SettingButtonsRow bind:configToEdit keys={['user']} {disabled} />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -19,11 +19,6 @@
|
||||
!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();
|
||||
|
||||
@@ -51,7 +46,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1">
|
||||
<Button shape="round" {disabled} size="small" color="secondary" onclick={handleReset}>{$t('reset')}</Button>
|
||||
<Button shape="round" {disabled} size="small" color="secondary" onclick={onCancel}>{$t('cancel')}</Button>
|
||||
<Button shape="round" type="submit" {disabled} size="small" onclick={handleSave}>{$t('save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mb-4 w-full">
|
||||
<div class="w-full">
|
||||
<div class="flex h-6.5 place-items-center gap-1">
|
||||
<label class="font-medium text-primary text-sm" for="{name}-select">
|
||||
{label}
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mb-4 w-full">
|
||||
<div class="w-full">
|
||||
<div class="flex place-items-center gap-1">
|
||||
<label class="font-medium text-primary text-sm min-h-6 uppercase" for={label}>{label}</label>
|
||||
{#if required}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="mb-4 w-full">
|
||||
<div class="w-full">
|
||||
<div class="flex h-6.5 place-items-center gap-1">
|
||||
<label class="font-medium text-primary text-sm" for="{name}-select">{label}</label>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
subtitle?: string | Snippet;
|
||||
checked?: boolean;
|
||||
disabled?: boolean;
|
||||
isEdited?: boolean;
|
||||
@@ -48,8 +48,10 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if subtitle}
|
||||
{#if typeof subtitle === 'string'}
|
||||
<p id={subtitleId} class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
|
||||
{:else}
|
||||
{@render subtitle?.()}
|
||||
{/if}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="mb-4 w-full">
|
||||
<div class="w-full">
|
||||
<div class="flex h-6.5 place-items-center gap-1">
|
||||
<label class="font-medium text-primary text-sm" for={label}>{label}</label>
|
||||
{#if required}
|
||||
|
||||
69
web/src/lib/modals/SystemSettingsModal.svelte
Normal file
69
web/src/lib/modals/SystemSettingsModal.svelte
Normal file
@@ -0,0 +1,69 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { getSystemConfigActions, handleSystemConfigSave } from '$lib/services/system-config.service';
|
||||
import type { SystemConfigContext } from '$lib/types';
|
||||
import type { SystemConfigDto } from '@immich/sdk';
|
||||
import { Button, FormModal, type ModalSize } from '@immich/ui';
|
||||
import { isEqual, pick } from 'lodash-es';
|
||||
import { type Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
keys: Array<keyof SystemConfigDto>;
|
||||
size?: ModalSize;
|
||||
onBeforeSave?: (context: SystemConfigContext) => Promise<boolean>;
|
||||
child: Snippet<[SystemConfigContext]>;
|
||||
};
|
||||
|
||||
let { keys, size = 'medium', onBeforeSave, child }: Props = $props();
|
||||
|
||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||
const config = $derived(systemConfigManager.value);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
const { settings } = $derived(getSystemConfigActions($t, featureFlagsManager.value, systemConfigManager.value));
|
||||
const setting = $derived(settings.find((setting) => setting.href === page.url.pathname));
|
||||
const showResetToDefault = $derived(!isEqual(pick(configToEdit, keys), pick(systemConfigManager.defaultValue, keys)));
|
||||
|
||||
const handleResetToDefault = () => {
|
||||
const defaultConfig = systemConfigManager.cloneDefaultValue();
|
||||
configToEdit = { ...configToEdit, ...pick(defaultConfig, keys) };
|
||||
};
|
||||
|
||||
const onSubmit = async () => {
|
||||
const shouldSave = await onBeforeSave?.({ disabled, config, configToEdit });
|
||||
if (shouldSave ?? true) {
|
||||
await handleSystemConfigSave(pick(configToEdit, keys));
|
||||
await onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const onClose = async () => {
|
||||
await goto(AppRoute.ADMIN_SETTINGS);
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if setting}
|
||||
<FormModal
|
||||
size={size as 'small' | 'medium'}
|
||||
title={setting.title}
|
||||
icon={setting.icon}
|
||||
preventDefault
|
||||
{onClose}
|
||||
{onSubmit}
|
||||
>
|
||||
<div class="flex flex-col gap-5">
|
||||
{@render child({ disabled, config, configToEdit })}
|
||||
{#if showResetToDefault}
|
||||
<div class="flex justify-end mt-4">
|
||||
<Button size="small" color="secondary" variant="ghost" onclick={handleResetToDefault}>
|
||||
{$t('reset_to_default')}
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</FormModal>
|
||||
{/if}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { downloadManager } from '$lib/managers/download-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
@@ -6,7 +7,30 @@ import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { getConfig, updateConfig, type ServerFeaturesDto, type SystemConfigDto } from '@immich/sdk';
|
||||
import { toastManager, type ActionItem } from '@immich/ui';
|
||||
import { mdiContentCopy, mdiDownload, mdiUpload } from '@mdi/js';
|
||||
import {
|
||||
mdiAccountOutline,
|
||||
mdiBackupRestore,
|
||||
mdiBellOutline,
|
||||
mdiBookshelf,
|
||||
mdiClockOutline,
|
||||
mdiContentCopy,
|
||||
mdiDatabaseOutline,
|
||||
mdiDownload,
|
||||
mdiFileDocumentOutline,
|
||||
mdiFolderOutline,
|
||||
mdiImageOutline,
|
||||
mdiLockOutline,
|
||||
mdiMapMarkerOutline,
|
||||
mdiPaletteOutline,
|
||||
mdiRestore,
|
||||
mdiRobotOutline,
|
||||
mdiServerOutline,
|
||||
mdiSync,
|
||||
mdiTrashCanOutline,
|
||||
mdiUpdate,
|
||||
mdiUpload,
|
||||
mdiVideoOutline,
|
||||
} from '@mdi/js';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
|
||||
@@ -15,6 +39,123 @@ export const getSystemConfigActions = (
|
||||
featureFlags: ServerFeaturesDto,
|
||||
config: SystemConfigDto,
|
||||
) => {
|
||||
const settings: Array<{ title: string; subtitle: string; href: string; icon: string }> = [
|
||||
{
|
||||
title: $t('admin.authentication_settings'),
|
||||
subtitle: $t('admin.authentication_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/authentication`,
|
||||
icon: mdiLockOutline,
|
||||
},
|
||||
{
|
||||
title: $t('admin.backup_settings'),
|
||||
subtitle: $t('admin.backup_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/backup`,
|
||||
icon: mdiBackupRestore,
|
||||
},
|
||||
{
|
||||
title: $t('admin.image_settings'),
|
||||
subtitle: $t('admin.image_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/image`,
|
||||
icon: mdiImageOutline,
|
||||
},
|
||||
{
|
||||
title: $t('admin.job_settings'),
|
||||
subtitle: $t('admin.job_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/job`,
|
||||
icon: mdiSync,
|
||||
},
|
||||
{
|
||||
title: $t('admin.library_settings'),
|
||||
subtitle: $t('admin.library_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/library`,
|
||||
icon: mdiBookshelf,
|
||||
},
|
||||
{
|
||||
title: $t('admin.logging_settings'),
|
||||
subtitle: $t('admin.manage_log_settings'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/logging`,
|
||||
icon: mdiFileDocumentOutline,
|
||||
},
|
||||
{
|
||||
title: $t('admin.machine_learning_settings'),
|
||||
subtitle: $t('admin.machine_learning_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/machine-learning`,
|
||||
icon: mdiRobotOutline,
|
||||
},
|
||||
{
|
||||
title: $t('admin.maintenance_settings'),
|
||||
subtitle: $t('admin.maintenance_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/maintenance`,
|
||||
icon: mdiRestore,
|
||||
},
|
||||
{
|
||||
title: $t('admin.map_gps_settings'),
|
||||
subtitle: $t('admin.map_gps_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/location`,
|
||||
icon: mdiMapMarkerOutline,
|
||||
},
|
||||
{
|
||||
title: $t('admin.metadata_settings'),
|
||||
subtitle: $t('admin.metadata_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/metadata`,
|
||||
icon: mdiDatabaseOutline,
|
||||
},
|
||||
{
|
||||
title: $t('admin.nightly_tasks_settings'),
|
||||
subtitle: $t('admin.nightly_tasks_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/nightly-tasks`,
|
||||
icon: mdiClockOutline,
|
||||
},
|
||||
{
|
||||
title: $t('admin.notification_settings'),
|
||||
subtitle: $t('admin.notification_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/notifications`,
|
||||
icon: mdiBellOutline,
|
||||
},
|
||||
{
|
||||
title: $t('admin.server_settings'),
|
||||
subtitle: $t('admin.server_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/server`,
|
||||
icon: mdiServerOutline,
|
||||
},
|
||||
{
|
||||
title: $t('admin.storage_template_settings'),
|
||||
subtitle: $t('admin.storage_template_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/storage-template`,
|
||||
icon: mdiFolderOutline,
|
||||
},
|
||||
{
|
||||
title: $t('admin.theme_settings'),
|
||||
subtitle: $t('admin.theme_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/theme`,
|
||||
icon: mdiPaletteOutline,
|
||||
},
|
||||
{
|
||||
title: $t('admin.trash_settings'),
|
||||
subtitle: $t('admin.trash_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/trash`,
|
||||
icon: mdiTrashCanOutline,
|
||||
},
|
||||
{
|
||||
title: $t('admin.user_settings'),
|
||||
subtitle: $t('admin.user_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/user`,
|
||||
icon: mdiAccountOutline,
|
||||
},
|
||||
{
|
||||
title: $t('admin.version_check_settings'),
|
||||
subtitle: $t('admin.version_check_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/version-check`,
|
||||
icon: mdiUpdate,
|
||||
},
|
||||
{
|
||||
title: $t('admin.transcoding_settings'),
|
||||
subtitle: $t('admin.transcoding_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
|
||||
icon: mdiVideoOutline,
|
||||
},
|
||||
];
|
||||
|
||||
const CopyToClipboard: ActionItem = {
|
||||
title: $t('copy_to_clipboard'),
|
||||
description: $t('admin.copy_config_to_clipboard_description'),
|
||||
@@ -46,7 +187,7 @@ export const getSystemConfigActions = (
|
||||
shortcuts: { shift: true, key: 'u' },
|
||||
};
|
||||
|
||||
return { CopyToClipboard, Download, Upload };
|
||||
return { settings, CopyToClipboard, Download, Upload };
|
||||
};
|
||||
|
||||
export const handleSystemConfigSave = async (update: Partial<SystemConfigDto>) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { QueueResponseDto, ServerVersionResponseDto } from '@immich/sdk';
|
||||
import type { QueueResponseDto, ServerVersionResponseDto, SystemConfigDto } from '@immich/sdk';
|
||||
import type { ActionItem } from '@immich/ui';
|
||||
|
||||
export interface ReleaseEvent {
|
||||
@@ -12,3 +12,5 @@ export interface ReleaseEvent {
|
||||
export type QueueSnapshot = { timestamp: number; snapshot?: QueueResponseDto[] };
|
||||
|
||||
export type HeaderButtonActionItem = ActionItem & { data?: { title?: string } };
|
||||
|
||||
export type SystemConfigContext = { disabled: boolean; config: SystemConfigDto; configToEdit: SystemConfigDto };
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
hide_password: $t('hide_password'),
|
||||
confirm: $t('confirm'),
|
||||
cancel: $t('cancel'),
|
||||
save: $t('save'),
|
||||
toast_success_title: $t('success'),
|
||||
toast_info_title: $t('info'),
|
||||
toast_warning_title: $t('warning'),
|
||||
|
||||
62
web/src/routes/admin/system-settings/+layout.svelte
Normal file
62
web/src/routes/admin/system-settings/+layout.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<script lang="ts">
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||
import SearchBar from '$lib/elements/SearchBar.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { getSystemConfigActions } from '$lib/services/system-config.service';
|
||||
import { Alert, Button, CommandPaletteContext, Icon, Text } from '@immich/ui';
|
||||
import { mdiPencilOutline } from '@mdi/js';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
type Props = {
|
||||
data: PageData;
|
||||
children?: Snippet;
|
||||
};
|
||||
|
||||
const { data, children }: Props = $props();
|
||||
|
||||
const { settings, CopyToClipboard, Upload, Download } = $derived(
|
||||
getSystemConfigActions($t, featureFlagsManager.value, systemConfigManager.value),
|
||||
);
|
||||
|
||||
let searchQuery = $state('');
|
||||
let filteredSettings = $derived(
|
||||
settings.filter(({ title, subtitle }) => {
|
||||
const query = searchQuery.toLowerCase();
|
||||
return title.toLowerCase().includes(query) || subtitle.toLowerCase().includes(query);
|
||||
}),
|
||||
);
|
||||
</script>
|
||||
|
||||
<CommandPaletteContext commands={[CopyToClipboard, Upload, Download]} />
|
||||
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]} actions={[CopyToClipboard, Download, Upload]}>
|
||||
<section id="setting-content" class="flex place-content-center sm:mx-4 mt-4">
|
||||
<section class="w-full pb-28 sm:w-5/6 md:w-4xl">
|
||||
{#if featureFlagsManager.value.configFile}
|
||||
<Alert color="warning" class="text-dark my-4" title={$t('admin.config_set_by_file')} />
|
||||
{/if}
|
||||
<div class="mb-4">
|
||||
<SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
{#each filteredSettings as { title, subtitle, href, icon } (href)}
|
||||
<Button variant="outline" color="secondary" class="flex justify-between border-subtle" {href}>
|
||||
<div class="flex flex-col items-start">
|
||||
<Text size="large" fontWeight="semi-bold" color="primary" class="flex items-center gap-2">
|
||||
<Icon {icon} />
|
||||
{title}
|
||||
</Text>
|
||||
<Text>{subtitle}</Text>
|
||||
</div>
|
||||
<Icon icon={mdiPencilOutline} size="1.5rem" />
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
</AdminPageLayout>
|
||||
|
||||
{@render children?.()}
|
||||
@@ -1,17 +1,12 @@
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { getConfig, getConfigDefaults } from '@immich/sdk';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ url }) => {
|
||||
await authenticate(url, { admin: true });
|
||||
const config = await getConfig();
|
||||
const defaultConfig = await getConfigDefaults();
|
||||
const $t = await getFormatter();
|
||||
|
||||
return {
|
||||
config,
|
||||
defaultConfig,
|
||||
meta: {
|
||||
title: $t('admin.system_settings'),
|
||||
},
|
||||
@@ -1,237 +0,0 @@
|
||||
<script lang="ts">
|
||||
import AuthSettings from '$lib/components/admin-settings/AuthSettings.svelte';
|
||||
import BackupSettings from '$lib/components/admin-settings/BackupSettings.svelte';
|
||||
import FFmpegSettings from '$lib/components/admin-settings/FFmpegSettings.svelte';
|
||||
import ImageSettings from '$lib/components/admin-settings/ImageSettings.svelte';
|
||||
import JobSettings from '$lib/components/admin-settings/JobSettings.svelte';
|
||||
import LibrarySettings from '$lib/components/admin-settings/LibrarySettings.svelte';
|
||||
import LoggingSettings from '$lib/components/admin-settings/LoggingSettings.svelte';
|
||||
import MachineLearningSettings from '$lib/components/admin-settings/MachineLearningSettings.svelte';
|
||||
import MaintenanceSettings from '$lib/components/admin-settings/MaintenanceSettings.svelte';
|
||||
import MapSettings from '$lib/components/admin-settings/MapSettings.svelte';
|
||||
import MetadataSettings from '$lib/components/admin-settings/MetadataSettings.svelte';
|
||||
import NewVersionCheckSettings from '$lib/components/admin-settings/NewVersionCheckSettings.svelte';
|
||||
import NightlyTasksSettings from '$lib/components/admin-settings/NightlyTasksSettings.svelte';
|
||||
import NotificationSettings from '$lib/components/admin-settings/NotificationSettings.svelte';
|
||||
import ServerSettings from '$lib/components/admin-settings/ServerSettings.svelte';
|
||||
import StorageTemplateSettings from '$lib/components/admin-settings/StorageTemplateSettings.svelte';
|
||||
import ThemeSettings from '$lib/components/admin-settings/ThemeSettings.svelte';
|
||||
import TrashSettings from '$lib/components/admin-settings/TrashSettings.svelte';
|
||||
import UserSettings from '$lib/components/admin-settings/UserSettings.svelte';
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.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 { QueryParameter } from '$lib/constants';
|
||||
import SearchBar from '$lib/elements/SearchBar.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
|
||||
import { getSystemConfigActions } from '$lib/services/system-config.service';
|
||||
import { Alert, CommandPaletteContext } from '@immich/ui';
|
||||
import {
|
||||
mdiAccountOutline,
|
||||
mdiBackupRestore,
|
||||
mdiBellOutline,
|
||||
mdiBookshelf,
|
||||
mdiClockOutline,
|
||||
mdiDatabaseOutline,
|
||||
mdiFileDocumentOutline,
|
||||
mdiFolderOutline,
|
||||
mdiImageOutline,
|
||||
mdiLockOutline,
|
||||
mdiMapMarkerOutline,
|
||||
mdiPaletteOutline,
|
||||
mdiRestore,
|
||||
mdiRobotOutline,
|
||||
mdiServerOutline,
|
||||
mdiSync,
|
||||
mdiTrashCanOutline,
|
||||
mdiUpdate,
|
||||
mdiVideoOutline,
|
||||
} from '@mdi/js';
|
||||
import type { Component } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
type Props = {
|
||||
data: PageData;
|
||||
};
|
||||
|
||||
const { data }: Props = $props();
|
||||
|
||||
const settings: Array<{
|
||||
component: Component;
|
||||
title: string;
|
||||
subtitle: string;
|
||||
key: string;
|
||||
icon: string;
|
||||
}> = [
|
||||
{
|
||||
component: AuthSettings,
|
||||
title: $t('admin.authentication_settings'),
|
||||
subtitle: $t('admin.authentication_settings_description'),
|
||||
key: 'authentication',
|
||||
icon: mdiLockOutline,
|
||||
},
|
||||
{
|
||||
component: BackupSettings,
|
||||
title: $t('admin.backup_settings'),
|
||||
subtitle: $t('admin.backup_settings_description'),
|
||||
key: 'backup',
|
||||
icon: mdiBackupRestore,
|
||||
},
|
||||
{
|
||||
component: ImageSettings,
|
||||
title: $t('admin.image_settings'),
|
||||
subtitle: $t('admin.image_settings_description'),
|
||||
key: 'image',
|
||||
icon: mdiImageOutline,
|
||||
},
|
||||
{
|
||||
component: JobSettings,
|
||||
title: $t('admin.job_settings'),
|
||||
subtitle: $t('admin.job_settings_description'),
|
||||
key: 'job',
|
||||
icon: mdiSync,
|
||||
},
|
||||
{
|
||||
component: LibrarySettings,
|
||||
title: $t('admin.library_settings'),
|
||||
subtitle: $t('admin.library_settings_description'),
|
||||
key: 'external-library',
|
||||
icon: mdiBookshelf,
|
||||
},
|
||||
{
|
||||
component: LoggingSettings,
|
||||
title: $t('admin.logging_settings'),
|
||||
subtitle: $t('admin.manage_log_settings'),
|
||||
key: 'logging',
|
||||
icon: mdiFileDocumentOutline,
|
||||
},
|
||||
{
|
||||
component: MachineLearningSettings,
|
||||
title: $t('admin.machine_learning_settings'),
|
||||
subtitle: $t('admin.machine_learning_settings_description'),
|
||||
key: 'machine-learning',
|
||||
icon: mdiRobotOutline,
|
||||
},
|
||||
{
|
||||
component: MaintenanceSettings,
|
||||
title: $t('admin.maintenance_settings'),
|
||||
subtitle: $t('admin.maintenance_settings_description'),
|
||||
key: 'maintenance',
|
||||
icon: mdiRestore,
|
||||
},
|
||||
{
|
||||
component: MapSettings,
|
||||
title: $t('admin.map_gps_settings'),
|
||||
subtitle: $t('admin.map_gps_settings_description'),
|
||||
key: 'location',
|
||||
icon: mdiMapMarkerOutline,
|
||||
},
|
||||
{
|
||||
component: MetadataSettings,
|
||||
title: $t('admin.metadata_settings'),
|
||||
subtitle: $t('admin.metadata_settings_description'),
|
||||
key: 'metadata',
|
||||
icon: mdiDatabaseOutline,
|
||||
},
|
||||
{
|
||||
component: NightlyTasksSettings,
|
||||
title: $t('admin.nightly_tasks_settings'),
|
||||
subtitle: $t('admin.nightly_tasks_settings_description'),
|
||||
key: 'nightly-tasks',
|
||||
icon: mdiClockOutline,
|
||||
},
|
||||
{
|
||||
component: NotificationSettings,
|
||||
title: $t('admin.notification_settings'),
|
||||
subtitle: $t('admin.notification_settings_description'),
|
||||
key: 'notifications',
|
||||
icon: mdiBellOutline,
|
||||
},
|
||||
{
|
||||
component: ServerSettings,
|
||||
title: $t('admin.server_settings'),
|
||||
subtitle: $t('admin.server_settings_description'),
|
||||
key: 'server',
|
||||
icon: mdiServerOutline,
|
||||
},
|
||||
{
|
||||
component: StorageTemplateSettings,
|
||||
title: $t('admin.storage_template_settings'),
|
||||
subtitle: $t('admin.storage_template_settings_description'),
|
||||
key: 'storage-template',
|
||||
icon: mdiFolderOutline,
|
||||
},
|
||||
{
|
||||
component: ThemeSettings,
|
||||
title: $t('admin.theme_settings'),
|
||||
subtitle: $t('admin.theme_settings_description'),
|
||||
key: 'theme',
|
||||
icon: mdiPaletteOutline,
|
||||
},
|
||||
{
|
||||
component: TrashSettings,
|
||||
title: $t('admin.trash_settings'),
|
||||
subtitle: $t('admin.trash_settings_description'),
|
||||
key: 'trash',
|
||||
icon: mdiTrashCanOutline,
|
||||
},
|
||||
{
|
||||
component: UserSettings,
|
||||
title: $t('admin.user_settings'),
|
||||
subtitle: $t('admin.user_settings_description'),
|
||||
key: 'user-settings',
|
||||
icon: mdiAccountOutline,
|
||||
},
|
||||
{
|
||||
component: NewVersionCheckSettings,
|
||||
title: $t('admin.version_check_settings'),
|
||||
subtitle: $t('admin.version_check_settings_description'),
|
||||
key: 'version-check',
|
||||
icon: mdiUpdate,
|
||||
},
|
||||
{
|
||||
component: FFmpegSettings,
|
||||
title: $t('admin.transcoding_settings'),
|
||||
subtitle: $t('admin.transcoding_settings_description'),
|
||||
key: 'video-transcoding',
|
||||
icon: mdiVideoOutline,
|
||||
},
|
||||
];
|
||||
|
||||
let searchQuery = $state('');
|
||||
|
||||
let filteredSettings = $derived(
|
||||
settings.filter(({ title, subtitle }) => {
|
||||
const query = searchQuery.toLowerCase();
|
||||
return title.toLowerCase().includes(query) || subtitle.toLowerCase().includes(query);
|
||||
}),
|
||||
);
|
||||
|
||||
const { CopyToClipboard, Upload, Download } = $derived(
|
||||
getSystemConfigActions($t, featureFlagsManager.value, systemConfigManager.value),
|
||||
);
|
||||
</script>
|
||||
|
||||
<CommandPaletteContext commands={[CopyToClipboard, Upload, Download]} />
|
||||
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]} actions={[CopyToClipboard, Download, Upload]}>
|
||||
<section id="setting-content" class="flex place-content-center sm:mx-4 mt-4">
|
||||
<section class="w-full pb-28 sm:w-5/6 md:w-4xl">
|
||||
{#if featureFlagsManager.value.configFile}
|
||||
<Alert color="warning" class="text-dark my-4" title={$t('admin.config_set_by_file')} />
|
||||
{/if}
|
||||
<div>
|
||||
<SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} />
|
||||
</div>
|
||||
<SettingAccordionState queryParam={QueryParameter.IS_OPEN}>
|
||||
{#each filteredSettings as { component: Component, title, subtitle, key, icon } (key)}
|
||||
<SettingAccordion {title} {subtitle} {key} {icon}>
|
||||
<Component />
|
||||
</SettingAccordion>
|
||||
{/each}
|
||||
</SettingAccordionState>
|
||||
</section>
|
||||
</section>
|
||||
</AdminPageLayout>
|
||||
|
||||
257
web/src/routes/admin/system-settings/authentication/+page.svelte
Normal file
257
web/src/routes/admin/system-settings/authentication/+page.svelte
Normal file
@@ -0,0 +1,257 @@
|
||||
<script lang="ts">
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import SystemSettingsCard from '$lib/components/SystemSettingsCard.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import FormatMessage from '$lib/elements/FormatMessage.svelte';
|
||||
import AuthDisableLoginConfirmModal from '$lib/modals/AuthDisableLoginConfirmModal.svelte';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import type { SystemConfigContext } from '$lib/types';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { OAuthTokenEndpointAuthMethod, unlinkAllOAuthAccountsAdmin, type SystemConfigDto } from '@immich/sdk';
|
||||
import { Button, modalManager, Text, toastManager } from '@immich/ui';
|
||||
import { mdiRestart } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
const handleToggleOverride = (configToEdit: SystemConfigDto) => {
|
||||
// click runs before bind
|
||||
const previouslyEnabled = configToEdit.oauth.mobileOverrideEnabled;
|
||||
if (!previouslyEnabled && !configToEdit.oauth.mobileRedirectUri) {
|
||||
configToEdit.oauth.mobileRedirectUri = globalThis.location.origin + '/api/oauth/mobile-redirect';
|
||||
}
|
||||
};
|
||||
|
||||
const onBeforeSave = async ({ configToEdit }: SystemConfigContext) => {
|
||||
const allMethodsDisabled = !configToEdit.oauth.enabled && !configToEdit.passwordLogin.enabled;
|
||||
if (allMethodsDisabled) {
|
||||
const isConfirmed = await modalManager.show(AuthDisableLoginConfirmModal);
|
||||
if (!isConfirmed) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleUnlinkAllOAuthAccounts = async () => {
|
||||
const confirmed = await modalManager.showDialog({
|
||||
icon: mdiRestart,
|
||||
title: $t('admin.unlink_all_oauth_accounts'),
|
||||
prompt: $t('admin.unlink_all_oauth_accounts_prompt'),
|
||||
confirmColor: 'danger',
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await unlinkAllOAuthAccountsAdmin();
|
||||
toastManager.success();
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.something_went_wrong'));
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['passwordLogin', 'oauth']} size="large" {onBeforeSave}>
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<SystemSettingsCard title={$t('admin.password_settings')} subtitle={$t('admin.password_settings_description')}>
|
||||
<SettingSwitch
|
||||
title={$t('admin.password_enable_description')}
|
||||
{disabled}
|
||||
bind:checked={configToEdit.passwordLogin.enabled}
|
||||
/>
|
||||
</SystemSettingsCard>
|
||||
|
||||
<SystemSettingsCard title={$t('admin.oauth_settings')} subtitle={$t('admin.oauth_settings_description')}>
|
||||
<Text size="small">
|
||||
<FormatMessage key="admin.oauth_settings_more_details">
|
||||
{#snippet children({ message })}
|
||||
<a href="https://docs.immich.app/administration/oauth" class="underline" target="_blank" rel="noreferrer">
|
||||
{message}
|
||||
</a>
|
||||
{/snippet}
|
||||
</FormatMessage>
|
||||
</Text>
|
||||
|
||||
<SettingSwitch
|
||||
{disabled}
|
||||
title={$t('admin.oauth_enable_description')}
|
||||
bind:checked={configToEdit.oauth.enabled}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Text size="small">{$t('admin.unlink_all_oauth_accounts_description')}</Text>
|
||||
<div>
|
||||
<Button size="small" color="secondary" onclick={handleUnlinkAllOAuthAccounts}
|
||||
>{$t('admin.unlink_all_oauth_accounts')}</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="ISSUER_URL"
|
||||
bind:value={configToEdit.oauth.issuerUrl}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.issuerUrl === config.oauth.issuerUrl)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="CLIENT_ID"
|
||||
bind:value={configToEdit.oauth.clientId}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.clientId === config.oauth.clientId)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="CLIENT_SECRET"
|
||||
description={$t('admin.oauth_client_secret_description')}
|
||||
bind:value={configToEdit.oauth.clientSecret}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.clientSecret === config.oauth.clientSecret)}
|
||||
/>
|
||||
|
||||
{#if configToEdit.oauth.clientSecret}
|
||||
<SettingSelect
|
||||
label="TOKEN_ENDPOINT_AUTH_METHOD"
|
||||
bind:value={configToEdit.oauth.tokenEndpointAuthMethod}
|
||||
disabled={disabled || !configToEdit.oauth.enabled || !configToEdit.oauth.clientSecret}
|
||||
isEdited={!(configToEdit.oauth.tokenEndpointAuthMethod === config.oauth.tokenEndpointAuthMethod)}
|
||||
options={[
|
||||
{ value: OAuthTokenEndpointAuthMethod.ClientSecretPost, text: 'client_secret_post' },
|
||||
{ value: OAuthTokenEndpointAuthMethod.ClientSecretBasic, text: 'client_secret_basic' },
|
||||
]}
|
||||
name="tokenEndpointAuthMethod"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="SCOPE"
|
||||
bind:value={configToEdit.oauth.scope}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.scope === config.oauth.scope)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="ID_TOKEN_SIGNED_RESPONSE_ALG"
|
||||
bind:value={configToEdit.oauth.signingAlgorithm}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.signingAlgorithm === config.oauth.signingAlgorithm)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="USERINFO_SIGNED_RESPONSE_ALG"
|
||||
bind:value={configToEdit.oauth.profileSigningAlgorithm}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.profileSigningAlgorithm === config.oauth.profileSigningAlgorithm)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.oauth_timeout')}
|
||||
description={$t('admin.oauth_timeout_description')}
|
||||
required={true}
|
||||
bind:value={configToEdit.oauth.timeout}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.timeout === config.oauth.timeout)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.oauth_storage_label_claim')}
|
||||
description={$t('admin.oauth_storage_label_claim_description')}
|
||||
bind:value={configToEdit.oauth.storageLabelClaim}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.storageLabelClaim === config.oauth.storageLabelClaim)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.oauth_role_claim')}
|
||||
description={$t('admin.oauth_role_claim_description')}
|
||||
bind:value={configToEdit.oauth.roleClaim}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.roleClaim === config.oauth.roleClaim)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.oauth_storage_quota_claim')}
|
||||
description={$t('admin.oauth_storage_quota_claim_description')}
|
||||
bind:value={configToEdit.oauth.storageQuotaClaim}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.storageQuotaClaim === config.oauth.storageQuotaClaim)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.oauth_storage_quota_default')}
|
||||
description={$t('admin.oauth_storage_quota_default_description')}
|
||||
bind:value={configToEdit.oauth.defaultStorageQuota}
|
||||
required={false}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.defaultStorageQuota === config.oauth.defaultStorageQuota)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.oauth_button_text')}
|
||||
bind:value={configToEdit.oauth.buttonText}
|
||||
required={false}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.buttonText === config.oauth.buttonText)}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title={$t('admin.oauth_auto_register')}
|
||||
subtitle={$t('admin.oauth_auto_register_description')}
|
||||
bind:checked={configToEdit.oauth.autoRegister}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title={$t('admin.oauth_auto_launch')}
|
||||
subtitle={$t('admin.oauth_auto_launch_description')}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
bind:checked={configToEdit.oauth.autoLaunch}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title={$t('admin.oauth_mobile_redirect_uri_override')}
|
||||
subtitle={$t('admin.oauth_mobile_redirect_uri_override_description', {
|
||||
values: { callback: 'app.immich:///oauth-callback' },
|
||||
})}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
onToggle={() => handleToggleOverride(configToEdit)}
|
||||
bind:checked={configToEdit.oauth.mobileOverrideEnabled}
|
||||
/>
|
||||
|
||||
{#if configToEdit.oauth.mobileOverrideEnabled}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.oauth_mobile_redirect_uri')}
|
||||
bind:value={configToEdit.oauth.mobileRedirectUri}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
isEdited={!(configToEdit.oauth.mobileRedirectUri === config.oauth.mobileRedirectUri)}
|
||||
/>
|
||||
{/if}
|
||||
</SystemSettingsCard>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
70
web/src/routes/admin/system-settings/backup/+page.svelte
Normal file
70
web/src/routes/admin/system-settings/backup/+page.svelte
Normal file
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import FormatMessage from '$lib/elements/FormatMessage.svelte';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
let cronExpressionOptions = $derived([
|
||||
{ text: $t('interval.night_at_midnight'), value: '0 0 * * *' },
|
||||
{ text: $t('interval.night_at_twoam'), value: '0 02 * * *' },
|
||||
{ text: $t('interval.day_at_onepm'), value: '0 13 * * *' },
|
||||
{ text: $t('interval.hours', { values: { hours: 6 } }), value: '0 */6 * * *' },
|
||||
]);
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['backup']}>
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<SettingSwitch
|
||||
title={$t('admin.backup_database_enable_description')}
|
||||
{disabled}
|
||||
bind:checked={configToEdit.backup.database.enabled}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
options={cronExpressionOptions}
|
||||
disabled={disabled || !configToEdit.backup.database.enabled}
|
||||
name="expression"
|
||||
label={$t('admin.cron_expression_presets')}
|
||||
bind:value={configToEdit.backup.database.cronExpression}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.backup.database.enabled}
|
||||
label={$t('admin.cron_expression')}
|
||||
bind:value={configToEdit.backup.database.cronExpression}
|
||||
isEdited={configToEdit.backup.database.cronExpression !== config.backup.database.cronExpression}
|
||||
>
|
||||
{#snippet descriptionSnippet()}
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
<FormatMessage key="admin.cron_expression_description">
|
||||
{#snippet children({ message })}
|
||||
<a
|
||||
href="https://crontab.guru/#{configToEdit.backup.database.cronExpression.replaceAll(' ', '_')}"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{message}
|
||||
<br />
|
||||
</a>
|
||||
{/snippet}
|
||||
</FormatMessage>
|
||||
</p>
|
||||
{/snippet}
|
||||
</SettingInputField>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
required={true}
|
||||
label={$t('admin.backup_keep_last_amount')}
|
||||
disabled={disabled || !configToEdit.backup.database.enabled}
|
||||
bind:value={configToEdit.backup.database.keepLastAmount}
|
||||
isEdited={configToEdit.backup.database.keepLastAmount !== config.backup.database.keepLastAmount}
|
||||
/>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
146
web/src/routes/admin/system-settings/image/+page.svelte
Normal file
146
web/src/routes/admin/system-settings/image/+page.svelte
Normal file
@@ -0,0 +1,146 @@
|
||||
<script lang="ts">
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import SystemSettingsCard from '$lib/components/SystemSettingsCard.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { Colorspace, ImageFormat } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['image']}>
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<SystemSettingsCard title={$t('admin.image_thumbnail_title')} subtitle={$t('admin.image_thumbnail_description')}>
|
||||
<SettingSelect
|
||||
label={$t('admin.image_format')}
|
||||
desc={$t('admin.image_format_description')}
|
||||
bind:value={configToEdit.image.thumbnail.format}
|
||||
options={[
|
||||
{ value: ImageFormat.Jpeg, text: 'JPEG' },
|
||||
{ value: ImageFormat.Webp, text: 'WebP' },
|
||||
]}
|
||||
name="format"
|
||||
isEdited={configToEdit.image.thumbnail.format !== config.image.thumbnail.format}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label={$t('admin.image_resolution')}
|
||||
desc={$t('admin.image_resolution_description')}
|
||||
number
|
||||
bind:value={configToEdit.image.thumbnail.size}
|
||||
options={[
|
||||
{ value: 1080, text: '1080p' },
|
||||
{ value: 720, text: '720p' },
|
||||
{ value: 480, text: '480p' },
|
||||
{ value: 250, text: '250p' },
|
||||
{ value: 200, text: '200p' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={configToEdit.image.thumbnail.size !== config.image.thumbnail.size}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.image_quality')}
|
||||
description={$t('admin.image_thumbnail_quality_description')}
|
||||
bind:value={configToEdit.image.thumbnail.quality}
|
||||
isEdited={configToEdit.image.thumbnail.quality !== config.image.thumbnail.quality}
|
||||
{disabled}
|
||||
/>
|
||||
</SystemSettingsCard>
|
||||
|
||||
<SystemSettingsCard title={$t('admin.image_preview_title')} subtitle={$t('admin.image_preview_description')}>
|
||||
<SettingSelect
|
||||
label={$t('admin.image_format')}
|
||||
desc={$t('admin.image_format_description')}
|
||||
bind:value={configToEdit.image.preview.format}
|
||||
options={[
|
||||
{ value: ImageFormat.Jpeg, text: 'JPEG' },
|
||||
{ value: ImageFormat.Webp, text: 'WebP' },
|
||||
]}
|
||||
name="format"
|
||||
isEdited={configToEdit.image.preview.format !== config.image.preview.format}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label={$t('admin.image_resolution')}
|
||||
desc={$t('admin.image_resolution_description')}
|
||||
number
|
||||
bind:value={configToEdit.image.preview.size}
|
||||
options={[
|
||||
{ value: 2160, text: '4K' },
|
||||
{ value: 1440, text: '1440p' },
|
||||
{ value: 1080, text: '1080p' },
|
||||
{ value: 720, text: '720p' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={configToEdit.image.preview.size !== config.image.preview.size}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.image_quality')}
|
||||
description={$t('admin.image_preview_quality_description')}
|
||||
bind:value={configToEdit.image.preview.quality}
|
||||
isEdited={configToEdit.image.preview.quality !== config.image.preview.quality}
|
||||
{disabled}
|
||||
/>
|
||||
</SystemSettingsCard>
|
||||
|
||||
<SystemSettingsCard title={$t('admin.image_fullsize_title')} subtitle={$t('admin.image_fullsize_description')}>
|
||||
<SettingSwitch
|
||||
title={$t('admin.image_fullsize_enabled')}
|
||||
subtitle={$t('admin.image_fullsize_enabled_description')}
|
||||
checked={configToEdit.image.fullsize.enabled}
|
||||
onToggle={(isChecked) => (configToEdit.image.fullsize.enabled = isChecked)}
|
||||
isEdited={configToEdit.image.fullsize.enabled !== config.image.fullsize.enabled}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label={$t('admin.image_format')}
|
||||
desc={$t('admin.image_format_description')}
|
||||
bind:value={configToEdit.image.fullsize.format}
|
||||
options={[
|
||||
{ value: ImageFormat.Jpeg, text: 'JPEG' },
|
||||
{ value: ImageFormat.Webp, text: 'WebP' },
|
||||
]}
|
||||
name="format"
|
||||
isEdited={configToEdit.image.fullsize.format !== config.image.fullsize.format}
|
||||
disabled={disabled || !configToEdit.image.fullsize.enabled}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.image_quality')}
|
||||
description={$t('admin.image_fullsize_quality_description')}
|
||||
bind:value={configToEdit.image.fullsize.quality}
|
||||
isEdited={configToEdit.image.fullsize.quality !== config.image.fullsize.quality}
|
||||
disabled={disabled || !configToEdit.image.fullsize.enabled}
|
||||
/>
|
||||
</SystemSettingsCard>
|
||||
|
||||
<SettingSwitch
|
||||
title={$t('admin.image_prefer_wide_gamut')}
|
||||
subtitle={$t('admin.image_prefer_wide_gamut_setting_description')}
|
||||
checked={configToEdit.image.colorspace === Colorspace.P3}
|
||||
onToggle={(isChecked) => (configToEdit.image.colorspace = isChecked ? Colorspace.P3 : Colorspace.Srgb)}
|
||||
isEdited={configToEdit.image.colorspace !== config.image.colorspace}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title={$t('admin.image_prefer_embedded_preview')}
|
||||
subtitle={$t('admin.image_prefer_embedded_preview_setting_description')}
|
||||
checked={configToEdit.image.extractEmbedded}
|
||||
onToggle={() => (configToEdit.image.extractEmbedded = !configToEdit.image.extractEmbedded)}
|
||||
isEdited={configToEdit.image.extractEmbedded !== config.image.extractEmbedded}
|
||||
{disabled}
|
||||
/>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
58
web/src/routes/admin/system-settings/job/+page.svelte
Normal file
58
web/src/routes/admin/system-settings/job/+page.svelte
Normal file
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { getQueueName } from '$lib/utils';
|
||||
import { QueueName, type SystemConfigDto, type SystemConfigJobDto } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
const queueNames = [
|
||||
QueueName.ThumbnailGeneration,
|
||||
QueueName.MetadataExtraction,
|
||||
QueueName.Library,
|
||||
QueueName.Sidecar,
|
||||
QueueName.SmartSearch,
|
||||
QueueName.FaceDetection,
|
||||
QueueName.FacialRecognition,
|
||||
QueueName.VideoConversion,
|
||||
QueueName.StorageTemplateMigration,
|
||||
QueueName.Migration,
|
||||
QueueName.Ocr,
|
||||
];
|
||||
|
||||
const isSystemConfigJobDto = (
|
||||
configToEdit: SystemConfigDto,
|
||||
jobName: string,
|
||||
): jobName is keyof SystemConfigJobDto => {
|
||||
return jobName in configToEdit.job;
|
||||
};
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['user']}>
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
{#each queueNames as queueName (queueName)}
|
||||
<div class="ms-4 mt-4 flex flex-col gap-4">
|
||||
{#if isSystemConfigJobDto(configToEdit, queueName)}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
{disabled}
|
||||
label={$t('admin.job_concurrency', { values: { job: $getQueueName(queueName) } })}
|
||||
description=""
|
||||
bind:value={configToEdit.job[queueName].concurrency}
|
||||
required={true}
|
||||
isEdited={!(configToEdit.job[queueName].concurrency == config.job[queueName].concurrency)}
|
||||
/>
|
||||
{:else}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.job_concurrency', { values: { job: $getQueueName(queueName) } })}
|
||||
description=""
|
||||
value={1}
|
||||
disabled={true}
|
||||
title={$t('admin.job_not_concurrency_safe')}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
20
web/src/routes/admin/system-settings/library/+page.svelte
Normal file
20
web/src/routes/admin/system-settings/library/+page.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['user']}>
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
min={1}
|
||||
label={$t('admin.user_delete_delay_settings')}
|
||||
description={$t('admin.user_delete_delay_settings_description')}
|
||||
bind:value={configToEdit.user.deleteDelay}
|
||||
{disabled}
|
||||
isEdited={configToEdit.user.deleteDelay !== config.user.deleteDelay}
|
||||
/>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
58
web/src/routes/admin/system-settings/location/+page.svelte
Normal file
58
web/src/routes/admin/system-settings/location/+page.svelte
Normal file
@@ -0,0 +1,58 @@
|
||||
<script lang="ts">
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import SystemSettingsCard from '$lib/components/SystemSettingsCard.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import FormatMessage from '$lib/elements/FormatMessage.svelte';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['map', 'reverseGeocoding']}>
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<div class="flex flex-col gap-4">
|
||||
<SystemSettingsCard title={$t('admin.map_settings')} subtitle={$t('admin.map_settings_description')}>
|
||||
<SettingSwitch title={$t('admin.map_enable_description')} {disabled} bind:checked={configToEdit.map.enabled} />
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.map_light_style')}
|
||||
description={$t('admin.map_style_description')}
|
||||
bind:value={configToEdit.map.lightStyle}
|
||||
disabled={disabled || !configToEdit.map.enabled}
|
||||
isEdited={configToEdit.map.lightStyle !== config.map.lightStyle}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.map_dark_style')}
|
||||
description={$t('admin.map_style_description')}
|
||||
bind:value={configToEdit.map.darkStyle}
|
||||
disabled={disabled || !configToEdit.map.enabled}
|
||||
isEdited={configToEdit.map.darkStyle !== config.map.darkStyle}
|
||||
/>
|
||||
</SystemSettingsCard>
|
||||
|
||||
<SystemSettingsCard title={$t('admin.map_reverse_geocoding_settings')}>
|
||||
{#snippet subtitle()}
|
||||
<FormatMessage key="admin.map_manage_reverse_geocoding_settings">
|
||||
{#snippet children({ message })}
|
||||
<a
|
||||
href="https://docs.immich.app/features/reverse-geocoding"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{message}
|
||||
</a>
|
||||
{/snippet}
|
||||
</FormatMessage>
|
||||
{/snippet}
|
||||
<SettingSwitch
|
||||
title={$t('admin.map_reverse_geocoding_enable_description')}
|
||||
{subtitle}
|
||||
{disabled}
|
||||
bind:checked={configToEdit.reverseGeocoding.enabled}
|
||||
/>
|
||||
</SystemSettingsCard>
|
||||
</div>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
33
web/src/routes/admin/system-settings/logging/+page.svelte
Normal file
33
web/src/routes/admin/system-settings/logging/+page.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { LogLevel } from '@immich/sdk';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['logging']}>
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<SettingSwitch
|
||||
title={$t('admin.logging_enable_description')}
|
||||
{disabled}
|
||||
bind:checked={configToEdit.logging.enabled}
|
||||
/>
|
||||
<SettingSelect
|
||||
label={$t('level')}
|
||||
desc={$t('admin.logging_level_description')}
|
||||
bind:value={configToEdit.logging.level}
|
||||
options={[
|
||||
{ value: LogLevel.Fatal, text: 'Fatal' },
|
||||
{ value: LogLevel.Error, text: 'Error' },
|
||||
{ value: LogLevel.Warn, text: 'Warn' },
|
||||
{ value: LogLevel.Log, text: 'Log' },
|
||||
{ value: LogLevel.Debug, text: 'Debug' },
|
||||
{ value: LogLevel.Verbose, text: 'Verbose' },
|
||||
]}
|
||||
name="level"
|
||||
isEdited={configToEdit.logging.level !== config.logging.level}
|
||||
disabled={disabled || !configToEdit.logging.enabled}
|
||||
/>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
@@ -0,0 +1,294 @@
|
||||
<script lang="ts">
|
||||
import SystemSettingsCard from '$lib/components/SystemSettingsCard.svelte';
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import FormatMessage from '$lib/elements/FormatMessage.svelte';
|
||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { Button, IconButton } from '@immich/ui';
|
||||
import { mdiPlus, mdiTrashCanOutline } from '@mdi/js';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['machineLearning']} size="large">
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<div class="flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.machine_learning_enabled')}
|
||||
subtitle={$t('admin.machine_learning_enabled_description')}
|
||||
{disabled}
|
||||
bind:checked={configToEdit.machineLearning.enabled}
|
||||
/>
|
||||
|
||||
<div>
|
||||
{#each configToEdit.machineLearning.urls as _, i (i)}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={i === 0 ? $t('url') : undefined}
|
||||
description={i === 0 ? $t('admin.machine_learning_url_description') : undefined}
|
||||
bind:value={configToEdit.machineLearning.urls[i]}
|
||||
required={i === 0}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled}
|
||||
isEdited={i === 0 && !isEqual(configToEdit.machineLearning.urls, config.machineLearning.urls)}
|
||||
>
|
||||
{#snippet trailingSnippet()}
|
||||
{#if configToEdit.machineLearning.urls.length > 1}
|
||||
<IconButton
|
||||
aria-label=""
|
||||
onclick={() => configToEdit.machineLearning.urls.splice(i, 1)}
|
||||
icon={mdiTrashCanOutline}
|
||||
color="danger"
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</SettingInputField>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<Button
|
||||
class="mb-2"
|
||||
size="small"
|
||||
shape="round"
|
||||
leadingIcon={mdiPlus}
|
||||
onclick={() => configToEdit.machineLearning.urls.push('')}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled}>{$t('add_url')}</Button
|
||||
>
|
||||
</div>
|
||||
|
||||
<SystemSettingsCard
|
||||
title={$t('admin.machine_learning_availability_checks')}
|
||||
subtitle={$t('admin.machine_learning_availability_checks_description')}
|
||||
>
|
||||
<SettingSwitch
|
||||
title={$t('admin.machine_learning_availability_checks_enabled')}
|
||||
bind:checked={configToEdit.machineLearning.availabilityChecks.enabled}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_availability_checks_interval')}
|
||||
bind:value={configToEdit.machineLearning.availabilityChecks.interval}
|
||||
description={$t('admin.machine_learning_availability_checks_interval_description')}
|
||||
disabled={disabled ||
|
||||
!configToEdit.machineLearning.enabled ||
|
||||
!configToEdit.machineLearning.availabilityChecks.enabled}
|
||||
isEdited={configToEdit.machineLearning.availabilityChecks.interval !==
|
||||
config.machineLearning.availabilityChecks.interval}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_availability_checks_timeout')}
|
||||
bind:value={configToEdit.machineLearning.availabilityChecks.timeout}
|
||||
description={$t('admin.machine_learning_availability_checks_timeout_description')}
|
||||
disabled={disabled ||
|
||||
!configToEdit.machineLearning.enabled ||
|
||||
!configToEdit.machineLearning.availabilityChecks.enabled}
|
||||
isEdited={configToEdit.machineLearning.availabilityChecks.timeout !==
|
||||
config.machineLearning.availabilityChecks.timeout}
|
||||
/>
|
||||
</SystemSettingsCard>
|
||||
|
||||
<SystemSettingsCard
|
||||
title={$t('admin.machine_learning_smart_search')}
|
||||
subtitle={$t('admin.machine_learning_smart_search_description')}
|
||||
>
|
||||
<SettingSwitch
|
||||
title={$t('admin.machine_learning_smart_search_enabled')}
|
||||
subtitle={$t('admin.machine_learning_smart_search_enabled_description')}
|
||||
bind:checked={configToEdit.machineLearning.clip.enabled}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.machine_learning_clip_model')}
|
||||
bind:value={configToEdit.machineLearning.clip.modelName}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.clip.enabled}
|
||||
isEdited={configToEdit.machineLearning.clip.modelName !== config.machineLearning.clip.modelName}
|
||||
>
|
||||
{#snippet descriptionSnippet()}
|
||||
<p class="immich-form-label pb-2 text-sm">
|
||||
<FormatMessage key="admin.machine_learning_clip_model_description">
|
||||
{#snippet children({ message })}
|
||||
<a target="_blank" href="https://huggingface.co/immich-app"><u>{message}</u></a>
|
||||
{/snippet}
|
||||
</FormatMessage>
|
||||
</p>
|
||||
{/snippet}
|
||||
</SettingInputField>
|
||||
</SystemSettingsCard>
|
||||
|
||||
<SystemSettingsCard
|
||||
title={$t('admin.machine_learning_duplicate_detection')}
|
||||
subtitle={$t('admin.machine_learning_duplicate_detection_setting_description')}
|
||||
>
|
||||
<SettingSwitch
|
||||
title={$t('admin.machine_learning_duplicate_detection_enabled')}
|
||||
subtitle={$t('admin.machine_learning_duplicate_detection_enabled_description')}
|
||||
bind:checked={configToEdit.machineLearning.duplicateDetection.enabled}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.clip.enabled}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_max_detection_distance')}
|
||||
bind:value={configToEdit.machineLearning.duplicateDetection.maxDistance}
|
||||
step="0.0005"
|
||||
min={0.001}
|
||||
max={0.1}
|
||||
description={$t('admin.machine_learning_max_detection_distance_description')}
|
||||
disabled={disabled || !featureFlagsManager.value.duplicateDetection}
|
||||
isEdited={configToEdit.machineLearning.duplicateDetection.maxDistance !==
|
||||
config.machineLearning.duplicateDetection.maxDistance}
|
||||
/>
|
||||
</SystemSettingsCard>
|
||||
|
||||
<SystemSettingsCard
|
||||
title={$t('admin.machine_learning_facial_recognition')}
|
||||
subtitle={$t('admin.machine_learning_facial_recognition_description')}
|
||||
>
|
||||
<SettingSwitch
|
||||
title={$t('admin.machine_learning_facial_recognition_setting')}
|
||||
subtitle={$t('admin.machine_learning_facial_recognition_setting_description')}
|
||||
bind:checked={configToEdit.machineLearning.facialRecognition.enabled}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label={$t('admin.machine_learning_facial_recognition_model')}
|
||||
desc={$t('admin.machine_learning_facial_recognition_model_description')}
|
||||
name="facial-recognition-model"
|
||||
bind:value={configToEdit.machineLearning.facialRecognition.modelName}
|
||||
options={[
|
||||
{ value: 'antelopev2', text: 'antelopev2' },
|
||||
{ value: 'buffalo_l', text: 'buffalo_l' },
|
||||
{ value: 'buffalo_m', text: 'buffalo_m' },
|
||||
{ value: 'buffalo_s', text: 'buffalo_s' },
|
||||
]}
|
||||
disabled={disabled ||
|
||||
!configToEdit.machineLearning.enabled ||
|
||||
!configToEdit.machineLearning.facialRecognition.enabled}
|
||||
isEdited={configToEdit.machineLearning.facialRecognition.modelName !==
|
||||
config.machineLearning.facialRecognition.modelName}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_min_detection_score')}
|
||||
description={$t('admin.machine_learning_min_detection_score_description')}
|
||||
bind:value={configToEdit.machineLearning.facialRecognition.minScore}
|
||||
step="0.01"
|
||||
min={0.1}
|
||||
max={1}
|
||||
disabled={disabled ||
|
||||
!configToEdit.machineLearning.enabled ||
|
||||
!configToEdit.machineLearning.facialRecognition.enabled}
|
||||
isEdited={configToEdit.machineLearning.facialRecognition.minScore !==
|
||||
config.machineLearning.facialRecognition.minScore}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_max_recognition_distance')}
|
||||
description={$t('admin.machine_learning_max_recognition_distance_description')}
|
||||
bind:value={configToEdit.machineLearning.facialRecognition.maxDistance}
|
||||
step="0.01"
|
||||
min={0.1}
|
||||
max={2}
|
||||
disabled={disabled ||
|
||||
!configToEdit.machineLearning.enabled ||
|
||||
!configToEdit.machineLearning.facialRecognition.enabled}
|
||||
isEdited={configToEdit.machineLearning.facialRecognition.maxDistance !==
|
||||
config.machineLearning.facialRecognition.maxDistance}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_min_recognized_faces')}
|
||||
description={$t('admin.machine_learning_min_recognized_faces_description')}
|
||||
bind:value={configToEdit.machineLearning.facialRecognition.minFaces}
|
||||
step="1"
|
||||
min={1}
|
||||
disabled={disabled ||
|
||||
!configToEdit.machineLearning.enabled ||
|
||||
!configToEdit.machineLearning.facialRecognition.enabled}
|
||||
isEdited={configToEdit.machineLearning.facialRecognition.minFaces !==
|
||||
config.machineLearning.facialRecognition.minFaces}
|
||||
/>
|
||||
</SystemSettingsCard>
|
||||
|
||||
<SystemSettingsCard
|
||||
title={$t('admin.machine_learning_ocr')}
|
||||
subtitle={$t('admin.machine_learning_ocr_description')}
|
||||
>
|
||||
<SettingSwitch
|
||||
title={$t('admin.machine_learning_ocr_enabled')}
|
||||
subtitle={$t('admin.machine_learning_ocr_enabled_description')}
|
||||
bind:checked={configToEdit.machineLearning.ocr.enabled}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label={$t('admin.machine_learning_ocr_model')}
|
||||
desc={$t('admin.machine_learning_ocr_model_description')}
|
||||
name="ocr-model"
|
||||
bind:value={configToEdit.machineLearning.ocr.modelName}
|
||||
options={[
|
||||
{ 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 (English-only)', value: 'EN__PP-OCRv5_mobile' },
|
||||
{ text: 'PP-OCRv5_mobile (Greek and English)', value: 'EL__PP-OCRv5_mobile' },
|
||||
{ text: 'PP-OCRv5_mobile (Korean and English)', value: 'KOREAN__PP-OCRv5_mobile' },
|
||||
{ text: 'PP-OCRv5_mobile (Latin script languages)', value: 'LATIN__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' },
|
||||
]}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.ocr.enabled}
|
||||
isEdited={configToEdit.machineLearning.ocr.modelName !== config.machineLearning.ocr.modelName}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_ocr_min_detection_score')}
|
||||
description={$t('admin.machine_learning_ocr_min_detection_score_description')}
|
||||
bind:value={configToEdit.machineLearning.ocr.minDetectionScore}
|
||||
step="0.1"
|
||||
min={0.1}
|
||||
max={1}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.ocr.enabled}
|
||||
isEdited={configToEdit.machineLearning.ocr.minDetectionScore !== config.machineLearning.ocr.minDetectionScore}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_ocr_min_recognition_score')}
|
||||
description={$t('admin.machine_learning_ocr_min_score_recognition_description')}
|
||||
bind:value={configToEdit.machineLearning.ocr.minRecognitionScore}
|
||||
step="0.1"
|
||||
min={0.1}
|
||||
max={1}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.ocr.enabled}
|
||||
isEdited={configToEdit.machineLearning.ocr.minRecognitionScore !==
|
||||
config.machineLearning.ocr.minRecognitionScore}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.machine_learning_ocr_max_resolution')}
|
||||
description={$t('admin.machine_learning_ocr_max_resolution_description')}
|
||||
bind:value={configToEdit.machineLearning.ocr.maxResolution}
|
||||
min={1}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.ocr.enabled}
|
||||
isEdited={configToEdit.machineLearning.ocr.maxResolution !== config.machineLearning.ocr.maxResolution}
|
||||
/>
|
||||
</SystemSettingsCard>
|
||||
</div>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
16
web/src/routes/admin/system-settings/metadata/+page.svelte
Normal file
16
web/src/routes/admin/system-settings/metadata/+page.svelte
Normal file
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['metadata']}>
|
||||
{#snippet child({ disabled, configToEdit })}
|
||||
<SettingSwitch
|
||||
title={$t('admin.metadata_faces_import_setting')}
|
||||
subtitle={$t('admin.metadata_faces_import_setting_description')}
|
||||
bind:checked={configToEdit.metadata.faces.import}
|
||||
{disabled}
|
||||
/>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
@@ -0,0 +1,53 @@
|
||||
<script lang="ts">
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['nightlyTasks']}>
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<div class="flex flex-col gap-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.nightly_tasks_start_time_setting')}
|
||||
description={$t('admin.nightly_tasks_start_time_setting_description')}
|
||||
bind:value={configToEdit.nightlyTasks.startTime}
|
||||
required={true}
|
||||
{disabled}
|
||||
isEdited={!(configToEdit.nightlyTasks.startTime === config.nightlyTasks.startTime)}
|
||||
/>
|
||||
<SettingSwitch
|
||||
title={$t('admin.nightly_tasks_database_cleanup_setting')}
|
||||
subtitle={$t('admin.nightly_tasks_database_cleanup_setting_description')}
|
||||
bind:checked={configToEdit.nightlyTasks.databaseCleanup}
|
||||
{disabled}
|
||||
/>
|
||||
<SettingSwitch
|
||||
title={$t('admin.nightly_tasks_missing_thumbnails_setting')}
|
||||
subtitle={$t('admin.nightly_tasks_missing_thumbnails_setting_description')}
|
||||
bind:checked={configToEdit.nightlyTasks.missingThumbnails}
|
||||
{disabled}
|
||||
/>
|
||||
<SettingSwitch
|
||||
title={$t('admin.nightly_tasks_cluster_new_faces_setting')}
|
||||
subtitle={$t('admin.nightly_tasks_cluster_faces_setting_description')}
|
||||
bind:checked={configToEdit.nightlyTasks.clusterNewFaces}
|
||||
{disabled}
|
||||
/>
|
||||
<SettingSwitch
|
||||
title={$t('admin.nightly_tasks_generate_memories_setting')}
|
||||
subtitle={$t('admin.nightly_tasks_generate_memories_setting_description')}
|
||||
bind:checked={configToEdit.nightlyTasks.generateMemories}
|
||||
{disabled}
|
||||
/>
|
||||
<SettingSwitch
|
||||
title={$t('admin.nightly_tasks_sync_quota_usage_setting')}
|
||||
subtitle={$t('admin.nightly_tasks_sync_quota_usage_setting_description')}
|
||||
bind:checked={configToEdit.nightlyTasks.syncQuotaUsage}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['user']}>
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
min={1}
|
||||
label={$t('admin.user_delete_delay_settings')}
|
||||
description={$t('admin.user_delete_delay_settings_description')}
|
||||
bind:value={configToEdit.user.deleteDelay}
|
||||
{disabled}
|
||||
isEdited={configToEdit.user.deleteDelay !== config.user.deleteDelay}
|
||||
/>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
34
web/src/routes/admin/system-settings/server/+page.svelte
Normal file
34
web/src/routes/admin/system-settings/server/+page.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['server']}>
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.server_external_domain_settings')}
|
||||
description={$t('admin.server_external_domain_settings_description')}
|
||||
bind:value={configToEdit.server.externalDomain}
|
||||
isEdited={configToEdit.server.externalDomain !== config.server.externalDomain}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label={$t('admin.server_welcome_message')}
|
||||
description={$t('admin.server_welcome_message_description')}
|
||||
bind:value={configToEdit.server.loginPageMessage}
|
||||
isEdited={configToEdit.server.loginPageMessage !== config.server.loginPageMessage}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title={$t('admin.server_public_users')}
|
||||
subtitle={$t('admin.server_public_users_description')}
|
||||
{disabled}
|
||||
bind:checked={configToEdit.server.publicUsers}
|
||||
/>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['user']}>
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
min={1}
|
||||
label={$t('admin.user_delete_delay_settings')}
|
||||
description={$t('admin.user_delete_delay_settings_description')}
|
||||
bind:value={configToEdit.user.deleteDelay}
|
||||
{disabled}
|
||||
isEdited={configToEdit.user.deleteDelay !== config.user.deleteDelay}
|
||||
/>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
17
web/src/routes/admin/system-settings/theme/+page.svelte
Normal file
17
web/src/routes/admin/system-settings/theme/+page.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import SettingTextarea from '$lib/components/shared-components/settings/setting-textarea.svelte';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['theme']}>
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<SettingTextarea
|
||||
{disabled}
|
||||
label={$t('admin.theme_custom_css_settings')}
|
||||
description={$t('admin.theme_custom_css_settings_description')}
|
||||
bind:value={configToEdit.theme.customCss}
|
||||
isEdited={configToEdit.theme.customCss !== config.theme.customCss}
|
||||
/>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
29
web/src/routes/admin/system-settings/trash/+page.svelte
Normal file
29
web/src/routes/admin/system-settings/trash/+page.svelte
Normal file
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['trash']}>
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<div class="flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title={$t('admin.trash_enabled_description')}
|
||||
{disabled}
|
||||
bind:checked={configToEdit.trash.enabled}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label={$t('admin.trash_number_of_days')}
|
||||
description={$t('admin.trash_number_of_days_description')}
|
||||
bind:value={configToEdit.trash.days}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.trash.enabled}
|
||||
isEdited={configToEdit.trash.days !== config.trash.days}
|
||||
/>
|
||||
</div>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
20
web/src/routes/admin/system-settings/user/+page.svelte
Normal file
20
web/src/routes/admin/system-settings/user/+page.svelte
Normal file
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['user']}>
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
min={1}
|
||||
label={$t('admin.user_delete_delay_settings')}
|
||||
description={$t('admin.user_delete_delay_settings_description')}
|
||||
bind:value={configToEdit.user.deleteDelay}
|
||||
{disabled}
|
||||
isEdited={configToEdit.user.deleteDelay !== config.user.deleteDelay}
|
||||
/>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script lang="ts">
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['newVersionCheck']}>
|
||||
{#snippet child({ disabled, configToEdit })}
|
||||
<SettingSwitch
|
||||
title={$t('admin.version_check_enabled_description')}
|
||||
subtitle={$t('admin.version_check_implications')}
|
||||
bind:checked={configToEdit.newVersionCheck.enabled}
|
||||
{disabled}
|
||||
/>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import { SettingInputFieldType } from '$lib/constants';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['user']}>
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
min={1}
|
||||
label={$t('admin.user_delete_delay_settings')}
|
||||
description={$t('admin.user_delete_delay_settings_description')}
|
||||
bind:value={configToEdit.user.deleteDelay}
|
||||
{disabled}
|
||||
isEdited={configToEdit.user.deleteDelay !== config.user.deleteDelay}
|
||||
/>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
Reference in New Issue
Block a user