feat/system-settings
Jason Rasmussen 2025-12-17 13:43:07 -05:00
parent 009a37f0a7
commit ef7a37a26a
No known key found for this signature in database
GPG Key ID: 75AD31BF84C94773
16 changed files with 942 additions and 624 deletions

View File

@ -272,7 +272,7 @@
"oauth_timeout_description": "Timeout for requests in milliseconds",
"ocr_job_description": "Use machine learning to recognize text in images",
"password_enable_description": "Login with email and password",
"password_settings": "Password Login",
"password_settings": "Password login",
"password_settings_description": "Manage password login settings",
"paths_validated_successfully": "All paths validated successfully",
"person_cleanup_job": "Person cleanup",

View File

@ -717,8 +717,8 @@ importers:
specifier: file:../open-api/typescript-sdk
version: link:../open-api/typescript-sdk
'@immich/ui':
specifier: ^0.50.0
version: 0.50.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)
specifier: ^0.50.1
version: 0.50.1(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)
'@mapbox/mapbox-gl-rtl-text':
specifier: 0.2.3
version: 0.2.3(mapbox-gl@1.13.3)
@ -2989,8 +2989,8 @@ packages:
peerDependencies:
svelte: ^5.0.0
'@immich/ui@0.50.0':
resolution: {integrity: sha512-7AW9SRZTAgal8xlkUAxm7o4+pSG7HcKb+Bh9JpWLaDRRdGyPCZMmsNa9CjZglOQ7wkAD07tQ9u4+zezBLe0dlQ==}
'@immich/ui@0.50.1':
resolution: {integrity: sha512-fNlQGh75ZFa/UZAgJaYk9/ItHOXHNNzN4CunjCmE7WocVVkUZbUxopN9Ku3F5GULSqD/zJ5gNO6PQAZ1ZoSaaQ==}
peerDependencies:
svelte: ^5.0.0
@ -14700,7 +14700,7 @@ snapshots:
dependencies:
svelte: 5.45.2
'@immich/ui@0.50.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)':
'@immich/ui@0.50.1(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)':
dependencies:
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.45.2)
'@internationalized/date': 3.10.0

View File

@ -28,7 +28,7 @@
"@formatjs/icu-messageformat-parser": "^2.9.8",
"@immich/justified-layout-wasm": "^0.4.3",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@immich/ui": "^0.50.0",
"@immich/ui": "^0.50.1",
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
"@mdi/js": "^7.4.47",
"@photo-sphere-viewer/core": "^5.14.0",

View File

@ -4,7 +4,7 @@
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 { getSystemConfigActions, handleSystemConfigSave, resolveSetting } 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';
@ -19,13 +19,13 @@
child: Snippet<[SystemConfigContext]>;
};
let { keys, size = 'medium', onBeforeSave, child }: Props = $props();
let { keys, size = 'large', 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 setting = $derived(resolveSetting(settings, page.url.pathname));
const showResetToDefault = $derived(!isEqual(pick(configToEdit, keys), pick(systemConfigManager.defaultValue, keys)));
const handleResetToDefault = () => {
@ -47,14 +47,7 @@
</script>
{#if setting}
<FormModal
size={size as 'small' | 'medium'}
title={setting.title}
icon={setting.icon}
preventDefault
{onClose}
{onSubmit}
>
<FormModal {size} title={setting.title} icon={setting.icon} preventDefault {onClose} {onSubmit}>
<div class="flex flex-col gap-5">
{@render child({ disabled, config, configToEdit })}
{#if showResetToDefault}

View File

@ -1,12 +1,14 @@
import { AppRoute } from '$lib/constants';
import { downloadManager } from '$lib/managers/download-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte';
import AuthDisableLoginConfirmModal from '$lib/modals/AuthDisableLoginConfirmModal.svelte';
import type { SystemConfigContext } from '$lib/types';
import { copyToClipboard } from '$lib/utils';
import { downloadBlob } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n';
import { getConfig, updateConfig, type ServerFeaturesDto, type SystemConfigDto } from '@immich/sdk';
import { toastManager, type ActionItem } from '@immich/ui';
import { getConfig, unlinkAllOAuthAccountsAdmin, updateConfig, type ServerFeaturesDto, type SystemConfigDto } from '@immich/sdk';
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
import {
mdiAccountOutline,
mdiBackupRestore,
@ -18,76 +20,86 @@ import {
mdiDownload,
mdiFileDocumentOutline,
mdiFolderOutline,
mdiImageOutline,
mdiLockOutline,
mdiMapMarkerOutline,
mdiPaletteOutline,
mdiRestore,
mdiRestart,
mdiRobotOutline,
mdiServerOutline,
mdiSync,
mdiTrashCanOutline,
mdiUpdate,
mdiUpload,
mdiVideoOutline,
mdiVideoOutline
} from '@mdi/js';
import { isEqual } from 'lodash-es';
import type { MessageFormatter } from 'svelte-i18n';
type SettingsGroup = {
title: string,
subtitle?: string;
items: SettingItem[];
}
type SettingItem = {
title: string; subtitle: string; href: string; icon: string;
};
export const resolveSetting = (groups: SettingsGroup[], pathname: string) => {
for (const group of groups) {
for (const item of group.items) {
if (item.href === pathname) {
return item;
}
}
}
}
export const getSystemConfigActions = (
$t: MessageFormatter,
featureFlags: ServerFeaturesDto,
config: SystemConfigDto,
) => {
const settings: Array<{ title: string; subtitle: string; href: string; icon: string }> = [
const settings: SettingsGroup[] = [
{
title: $t('admin.authentication_settings'),
subtitle: $t('admin.authentication_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/authentication`,
items: [
{
title:$t('admin.password_settings'), subtitle: $t('admin.password_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/password`,
icon: mdiLockOutline,
},
{
title: $t('admin.backup_settings'),
subtitle: $t('admin.backup_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/backup`,
icon: mdiBackupRestore,
title:$t('admin.oauth_settings'), subtitle:$t('admin.oauth_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/oauth`,
icon: mdiFileDocumentOutline,
},
]
},
{
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: 'General', items: [
// {
// title: $t('admin.image_settings'),
// subtitle: $t('admin.image_settings_description'),
// href: `${AppRoute.ADMIN_SETTINGS}/image`,
// icon: mdiImageOutline,
// },
{
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.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'),
@ -100,24 +112,12 @@ export const getSystemConfigActions = (
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'),
@ -142,11 +142,63 @@ export const getSystemConfigActions = (
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: 'Image', items: [
{
title: 'General settings',
subtitle: $t('admin.transcoding_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
icon: mdiVideoOutline,
},
{
title: 'Thumbnail settings',
subtitle: $t('admin.transcoding_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
icon: mdiVideoOutline,
},
{
title: 'Preview settings',
subtitle: $t('admin.transcoding_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
icon: mdiVideoOutline,
},
{
title: 'Full-size settings',
subtitle: $t('admin.transcoding_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
icon: mdiVideoOutline,
},
]
},
{
title: 'Video', items: [
{
title: 'General settings',
subtitle: $t('admin.transcoding_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
icon: mdiVideoOutline,
},
{
title: 'Transcoding Policies',
subtitle: $t('admin.transcoding_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
icon: mdiVideoOutline,
},
{
title: 'Hardware Acceleration',
subtitle: $t('admin.transcoding_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
icon: mdiVideoOutline,
},
{
title: 'Encoding Options',
subtitle: $t('admin.transcoding_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
icon: mdiVideoOutline,
},
{
title: $t('admin.transcoding_settings'),
@ -154,6 +206,108 @@ export const getSystemConfigActions = (
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
icon: mdiVideoOutline,
},
{
title: 'Advanced Settings',
subtitle: $t('admin.transcoding_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
icon: mdiVideoOutline,
},
]
},
{
title: 'Machine learning', items: [
{
title: 'Connection settings',
subtitle: $t('admin.machine_learning_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/machine-learning`,
icon: mdiRobotOutline,
},
// {
// title: $t('admin.machine_learning_settings'),
// subtitle: $t('admin.machine_learning_settings_description'),
// href: `${AppRoute.ADMIN_SETTINGS}/machine-learning`,
// icon: mdiRobotOutline,
// },
{
title: 'Search',
subtitle: $t('admin.machine_learning_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/machine-learning`,
icon: mdiRobotOutline,
},
{
title: 'Duplicate Detection',
subtitle: $t('admin.machine_learning_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/machine-learning`,
icon: mdiRobotOutline,
},
{
title: 'Facial Recognition',
subtitle: $t('admin.machine_learning_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/machine-learning`,
icon: mdiRobotOutline,
},
{
title: 'OCR',
subtitle: $t('admin.machine_learning_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/machine-learning`,
icon: mdiRobotOutline,
},
]
},
{
title: 'Job settings', items: [
{
title: 'General settings',
subtitle: $t('admin.server_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/server`,
icon: mdiServerOutline,
},
{
title: $t('admin.nightly_tasks_settings'),
subtitle: $t('admin.nightly_tasks_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/nightly-tasks`,
icon: mdiClockOutline,
},
]
},
{
title: 'Server settings', items: [
{
title: 'General settings',
subtitle: $t('admin.server_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/server`,
icon: mdiServerOutline,
},
{
title: $t('admin.authentication_settings'),
subtitle: $t('admin.authentication_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/authentication`,
icon: mdiLockOutline,
},
{
title: $t('admin.job_settings'),
subtitle: $t('admin.job_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/job`,
icon: mdiSync,
},
{
title: $t('admin.backup_settings'),
subtitle: $t('admin.backup_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/backup`,
icon: mdiBackupRestore,
},
{
title: $t('admin.version_check_settings'),
subtitle: $t('admin.version_check_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/version-check`,
icon: mdiUpdate,
},
]
},
];
const CopyToClipboard: ActionItem = {
@ -255,3 +409,38 @@ export const handleUploadConfig = () => {
});
input.remove();
};
export const handleUnlinkAllOAuthAccounts = async () => {
const $t = await getFormatter();
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'));
}
};
export 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;
};

View File

@ -4,7 +4,7 @@
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 { Alert, Button, Card, CardHeader, CardTitle, CommandPaletteContext, Container, Icon, Text } from '@immich/ui';
import { mdiPencilOutline } from '@mdi/js';
import type { Snippet } from 'svelte';
import { t } from 'svelte-i18n';
@ -22,39 +22,69 @@
);
let searchQuery = $state('');
let filteredSettings = $derived(
settings.filter(({ title, subtitle }) => {
const filteredGroups = $derived(
settings
.map(({ title, items }) => {
const query = searchQuery.toLowerCase();
return title.toLowerCase().includes(query) || subtitle.toLowerCase().includes(query);
}),
return {
title,
items: items.filter(
({ title, subtitle }) => title.toLowerCase().includes(query) || subtitle.toLowerCase().includes(query),
),
};
})
.filter(({ items }) => items.length > 0),
);
</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">
<section class="flex place-content-center sm:px-4 mt-4">
<section class="w-full pb-28">
<Container size="medium" center>
{#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 gap-8">
{#each filteredGroups as { title, items } (title)}
<div>
<Card color="secondary">
<CardHeader class="px-5 py-3">
<CardTitle>
<Text color="primary" fontWeight="semi-bold">{title}</Text>
</CardTitle>
</CardHeader>
<div>
{#each items as { title, subtitle, href, icon }, i (i)}
<Button
variant="outline"
shape="rectangle"
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} />
<Text fontWeight="semi-bold" class="flex items-center gap-2">
<!-- <Icon {icon} /> -->
{title}
</Text>
<Text>{subtitle}</Text>
<Text class="line-clamp-1" color="muted">{subtitle}</Text>
</div>
<Icon icon={mdiPencilOutline} size="1.5rem" />
<Icon icon={mdiPencilOutline} size="1.25rem" />
</Button>
{/each}
</div>
</Card>
</div>
{/each}
</div>
</Container>
</section>
</section>
</AdminPageLayout>

View File

@ -1,16 +1,13 @@
<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 { Button, Field, Input, modalManager, NumberInput, Switch, Text, toastManager } from '@immich/ui';
import { mdiRestart } from '@mdi/js';
import { t } from 'svelte-i18n';
@ -58,11 +55,9 @@
<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}
/>
<Field label={$t('admin.password_enable_description')} {disabled}>
<Switch bind:checked={configToEdit.passwordLogin.enabled} />
</Field>
</SystemSettingsCard>
<SystemSettingsCard title={$t('admin.oauth_settings')} subtitle={$t('admin.oauth_settings_description')}>
@ -76,11 +71,9 @@
</FormatMessage>
</Text>
<SettingSwitch
{disabled}
title={$t('admin.oauth_enable_description')}
bind:checked={configToEdit.oauth.enabled}
/>
<Field label={$t('admin.oauth_enable_description')} {disabled}>
<Switch bind:checked={configToEdit.oauth.enabled} />
</Field>
<div class="flex flex-col gap-2">
<Text size="small">{$t('admin.unlink_all_oauth_accounts_description')}</Text>
@ -91,32 +84,17 @@
</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)}
/>
<Field label="ISSUER_URL" required disabled={disabled || !configToEdit.oauth.enabled}>
<Input bind:value={configToEdit.oauth.issuerUrl} />
</Field>
<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)}
/>
<Field label="CLIENT_ID" required disabled={disabled || !configToEdit.oauth.enabled}>
<Input bind:value={configToEdit.oauth.clientId} />
</Field>
<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)}
/>
<Field label="CLIENT_SECRET" required disabled={disabled || !configToEdit.oauth.enabled}>
<Input bind:value={configToEdit.oauth.clientSecret} />
</Field>
{#if configToEdit.oauth.clientSecret}
<SettingSelect
@ -132,125 +110,108 @@
/>
{/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)}
/>
<Field label="SCOPE" required disabled={disabled || !configToEdit.oauth.enabled}>
<Input bind:value={configToEdit.oauth.scope} />
</Field>
<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)}
/>
<Field label="ID_TOKEN_SIGNED_RESPONSE_ALG" required disabled={disabled || !configToEdit.oauth.enabled}>
<Input bind:value={configToEdit.oauth.signingAlgorithm} />
</Field>
<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)}
/>
<Field label="USERINFO_SIGNED_RESPONSE_ALG" required disabled={disabled || !configToEdit.oauth.enabled}>
<Input bind:value={configToEdit.oauth.profileSigningAlgorithm} />
</Field>
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
<Field
label={$t('admin.oauth_timeout')}
description={$t('admin.oauth_timeout_description')}
required={true}
bind:value={configToEdit.oauth.timeout}
required
disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(configToEdit.oauth.timeout === config.oauth.timeout)}
/>
>
<NumberInput bind:value={configToEdit.oauth.timeout} />
</Field>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
<Field
label={$t('admin.oauth_storage_label_claim')}
description={$t('admin.oauth_storage_label_claim_description')}
bind:value={configToEdit.oauth.storageLabelClaim}
required={true}
required
disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(configToEdit.oauth.storageLabelClaim === config.oauth.storageLabelClaim)}
/>
>
<Input bind:value={configToEdit.oauth.storageLabelClaim} />
</Field>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
<Field
label={$t('admin.oauth_role_claim')}
description={$t('admin.oauth_role_claim_description')}
bind:value={configToEdit.oauth.roleClaim}
required={true}
required
disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(configToEdit.oauth.roleClaim === config.oauth.roleClaim)}
/>
>
<Input bind:value={configToEdit.oauth.roleClaim} />
</Field>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
<Field
label={$t('admin.oauth_storage_quota_claim')}
description={$t('admin.oauth_storage_quota_claim_description')}
bind:value={configToEdit.oauth.storageQuotaClaim}
required={true}
required
disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(configToEdit.oauth.storageQuotaClaim === config.oauth.storageQuotaClaim)}
/>
>
<Input bind:value={configToEdit.oauth.storageQuotaClaim} />
</Field>
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
<Field
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)}
>
<NumberInput
bind:value={
() => configToEdit.oauth.defaultStorageQuota ?? undefined,
(value) => (configToEdit.oauth.defaultStorageQuota = value ?? null)
}
/>
</Field>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label={$t('admin.oauth_button_text')}
bind:value={configToEdit.oauth.buttonText}
required={false}
<Field label={$t('admin.oauth_button_text')} disabled={disabled || !configToEdit.oauth.enabled}>
<Input bind:value={configToEdit.oauth.buttonText} />
</Field>
<Field
label={$t('admin.oauth_auto_register')}
description={$t('admin.oauth_auto_register_description')}
disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(configToEdit.oauth.buttonText === config.oauth.buttonText)}
/>
>
<Switch bind:checked={configToEdit.oauth.autoRegister} />
</Field>
<SettingSwitch
title={$t('admin.oauth_auto_register')}
subtitle={$t('admin.oauth_auto_register_description')}
bind:checked={configToEdit.oauth.autoRegister}
<Field
label={$t('admin.oauth_auto_launch')}
description={$t('admin.oauth_auto_launch_description')}
disabled={disabled || !configToEdit.oauth.enabled}
/>
>
<Switch bind:checked={configToEdit.oauth.autoLaunch} />
</Field>
<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', {
<Field
label={$t('admin.oauth_mobile_redirect_uri_override')}
description={$t('admin.oauth_mobile_redirect_uri_override_description', {
values: { callback: 'app.immich:///oauth-callback' },
})}
disabled={disabled || !configToEdit.oauth.enabled}
onToggle={() => handleToggleOverride(configToEdit)}
>
<Switch
bind:checked={configToEdit.oauth.mobileOverrideEnabled}
onCheckedChange={() => handleToggleOverride(configToEdit)}
/>
</Field>
{#if configToEdit.oauth.mobileOverrideEnabled}
<SettingInputField
inputType={SettingInputFieldType.TEXT}
<Field
label={$t('admin.oauth_mobile_redirect_uri')}
bind:value={configToEdit.oauth.mobileRedirectUri}
required={true}
required
disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(configToEdit.oauth.mobileRedirectUri === config.oauth.mobileRedirectUri)}
/>
>
<Input bind:value={configToEdit.oauth.mobileRedirectUri} />
</Field>
{/if}
</SystemSettingsCard>
{/snippet}

View File

@ -1,10 +1,8 @@
<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 { Field, HelperText, Input, Link, NumberInput, Switch } from '@immich/ui';
import { t } from 'svelte-i18n';
let cronExpressionOptions = $derived([
@ -16,12 +14,10 @@
</script>
<SystemSettingsModal keys={['backup']}>
{#snippet child({ disabled, config, configToEdit })}
<SettingSwitch
title={$t('admin.backup_database_enable_description')}
{disabled}
bind:checked={configToEdit.backup.database.enabled}
/>
{#snippet child({ disabled, configToEdit })}
<Field label={$t('admin.backup_database_enable_description')} {disabled}>
<Switch bind:checked={configToEdit.backup.database.enabled} />
</Field>
<SettingSelect
options={cronExpressionOptions}
@ -31,40 +27,25 @@
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">
<Field label={$t('admin.cron_expression')} required disabled={disabled || !configToEdit.backup.database.enabled}>
<Input bind:value={configToEdit.backup.database.cronExpression} />
<HelperText>
<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"
>
<Link href="https://crontab.guru/#{configToEdit.backup.database.cronExpression.replaceAll(' ', '_')}">
{message}
<br />
</a>
</Link>
{/snippet}
</FormatMessage>
</p>
{/snippet}
</SettingInputField>
</HelperText>
</Field>
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
required={true}
<Field
required
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}
/>
>
<NumberInput bind:value={configToEdit.backup.database.keepLastAmount}></NumberInput>
</Field>
{/snippet}
</SystemSettingsModal>

View File

@ -1,9 +1,8 @@
<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 { Field, HelperText, NumberInput } from '@immich/ui';
import { t } from 'svelte-i18n';
const queueNames = [
@ -29,30 +28,23 @@
</script>
<SystemSettingsModal keys={['user']}>
{#snippet child({ disabled, config, configToEdit })}
{#snippet child({ disabled, 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}
<Field
required
{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)}
/>
>
<NumberInput bind:value={configToEdit.job[queueName].concurrency} />
</Field>
{: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')}
/>
<Field label={$t('admin.job_concurrency', { values: { job: $getQueueName(queueName) } })}>
<NumberInput value={1} disabled={true} />
<HelperText>{$t('admin.job_not_concurrency_safe')}</HelperText>
</Field>
{/if}
</div>
{/each}
{/snippet}
</SystemSettingsModal>

View File

@ -1,20 +1,17 @@
<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 { Field, NumberInput } from '@immich/ui';
import { t } from 'svelte-i18n';
</script>
<SystemSettingsModal keys={['user']}>
{#snippet child({ disabled, config, configToEdit })}
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
min={1}
{#snippet child({ disabled, configToEdit })}
<Field
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}
/>
>
<NumberInput bind:value={configToEdit.user.deleteDelay} min={1} />
</Field>
{/snippet}
</SystemSettingsModal>

View File

@ -1,18 +1,17 @@
<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 { Field, Switch } from '@immich/ui';
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}
/>
<Field required {disabled} label={$t('admin.logging_enable_description')}>
<Switch bind:checked={configToEdit.logging.enabled} />
</Field>
<SettingSelect
label={$t('level')}
desc={$t('admin.logging_level_description')}

View File

@ -1,52 +1,59 @@
<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 {
Button,
Field,
HelperText,
IconButton,
Input,
Label,
Link,
NumberInput,
Stack,
Switch,
Text,
} 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')}
<Field
label={$t('admin.machine_learning_enabled')}
description={$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()}
<Switch bind:checked={configToEdit.machineLearning.enabled} />
</Field>
<Label label={$t('url')} />
<Text size="small" color="muted">{$t('admin.machine_learning_url_description')}</Text>
<Stack>
{#each configToEdit.machineLearning.urls as _, i (i)}
<Input
bind:value={configToEdit.machineLearning.urls[i]}
disabled={disabled || !configToEdit.machineLearning.enabled}
>
{#snippet trailingIcon()}
{#if configToEdit.machineLearning.urls.length > 1}
<IconButton
aria-label=""
size="small"
aria-label={$t('remove')}
onclick={() => configToEdit.machineLearning.urls.splice(i, 1)}
icon={mdiTrashCanOutline}
color="danger"
/>
{/if}
{/snippet}
</SettingInputField>
</Input>
{/each}
</div>
</Stack>
<div class="flex justify-end">
<Button
@ -63,103 +70,99 @@
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}
<Field
label={$t('admin.machine_learning_availability_checks_enabled')}
disabled={disabled || !configToEdit.machineLearning.enabled}
/>
>
<Switch bind:checked={configToEdit.machineLearning.availabilityChecks.enabled} />
</Field>
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
<Field
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}
/>
>
<NumberInput bind:value={configToEdit.machineLearning.availabilityChecks.interval} />
</Field>
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
<Field
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}
/>
>
<NumberInput bind:value={configToEdit.machineLearning.availabilityChecks.timeout} />
</Field>
</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}
<Field
label={$t('admin.machine_learning_smart_search_enabled')}
description={$t('admin.machine_learning_smart_search_enabled_description')}
disabled={disabled || !configToEdit.machineLearning.enabled}
/>
>
<Switch bind:checked={configToEdit.machineLearning.clip.enabled} />
</Field>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
<Field
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">
<Input bind:value={configToEdit.machineLearning.clip.modelName} />
<HelperText>
<FormatMessage key="admin.machine_learning_clip_model_description">
{#snippet children({ message })}
<a target="_blank" href="https://huggingface.co/immich-app"><u>{message}</u></a>
<Link href="https://huggingface.co/immich-app"><u>{message}</u></Link>
{/snippet}
</FormatMessage>
</p>
{/snippet}
</SettingInputField>
</HelperText>
</Field>
</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}
<Field
label={$t('admin.machine_learning_duplicate_detection_enabled')}
description={$t('admin.machine_learning_duplicate_detection_enabled_description')}
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.clip.enabled}
/>
>
<Switch bind:checked={configToEdit.machineLearning.duplicateDetection.enabled} />
</Field>
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
<Field
label={$t('admin.machine_learning_max_detection_distance')}
description={$t('admin.machine_learning_max_detection_distance_description')}
disabled={disabled || !featureFlagsManager.value.duplicateDetection}
>
<NumberInput
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}
/>
</Field>
</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}
<Field
label={$t('admin.machine_learning_facial_recognition_setting')}
description={$t('admin.machine_learning_facial_recognition_setting_description')}
disabled={disabled || !configToEdit.machineLearning.enabled}
/>
>
<Switch bind:checked={configToEdit.machineLearning.facialRecognition.enabled} />
</Field>
<SettingSelect
label={$t('admin.machine_learning_facial_recognition_model')}
@ -179,61 +182,57 @@
config.machineLearning.facialRecognition.modelName}
/>
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
<Field
label={$t('admin.machine_learning_min_detection_score')}
description={$t('admin.machine_learning_min_detection_score_description')}
disabled={disabled ||
!configToEdit.machineLearning.enabled ||
!configToEdit.machineLearning.facialRecognition.enabled}
>
<NumberInput
bind:value={configToEdit.machineLearning.facialRecognition.minScore}
step="0.01"
min={0.1}
max={1}
/>
</Field>
<Field
label={$t('admin.machine_learning_max_recognition_distance')}
description={$t('admin.machine_learning_max_recognition_distance_description')}
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')}
>
<NumberInput
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}
</Field>
<Field
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}
/>
>
<NumberInput bind:value={configToEdit.machineLearning.facialRecognition.minFaces} step="1" min={1} />
</Field>
</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}
<Field
label={$t('admin.machine_learning_ocr_enabled')}
description={$t('admin.machine_learning_ocr_enabled_description')}
disabled={disabled || !configToEdit.machineLearning.enabled}
/>
>
<Switch bind:checked={configToEdit.machineLearning.ocr.enabled} />
</Field>
<SettingSelect
label={$t('admin.machine_learning_ocr_model')}
@ -254,41 +253,29 @@
isEdited={configToEdit.machineLearning.ocr.modelName !== config.machineLearning.ocr.modelName}
/>
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
<Field
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}
/>
>
<NumberInput bind:value={configToEdit.machineLearning.ocr.minDetectionScore} step="0.1" min={0.1} max={1} />
</Field>
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
<Field
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}
/>
>
<NumberInput bind:value={configToEdit.machineLearning.ocr.minRecognitionScore} step="0.1" min={0.1} max={1} />
</Field>
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
<Field
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}
/>
>
<NumberInput bind:value={configToEdit.machineLearning.ocr.maxResolution} min={1} />
</Field>
</SystemSettingsCard>
</div>
{/snippet}
</SystemSettingsModal>

View File

@ -3,6 +3,7 @@
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { SettingInputFieldType } from '$lib/constants';
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
import { Field, Switch } from '@immich/ui';
import { t } from 'svelte-i18n';
</script>
@ -18,12 +19,13 @@
{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}
<Field
label={$t('admin.nightly_tasks_database_cleanup_setting')}
description={$t('admin.nightly_tasks_database_cleanup_setting_description')}
{disabled}
/>
>
<Switch bind:checked={configToEdit.nightlyTasks.databaseCleanup} />
</Field>
<SettingSwitch
title={$t('admin.nightly_tasks_missing_thumbnails_setting')}
subtitle={$t('admin.nightly_tasks_missing_thumbnails_setting_description')}

View File

@ -0,0 +1,173 @@
<script lang="ts">
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import FormatMessage from '$lib/elements/FormatMessage.svelte';
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
import { handleUnlinkAllOAuthAccounts, onBeforeSave } from '$lib/services/system-config.service';
import { OAuthTokenEndpointAuthMethod, type SystemConfigDto } from '@immich/sdk';
import { Alert, Button, Field, Input, NumberInput, Switch, Text } from '@immich/ui';
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';
}
};
</script>
<SystemSettingsModal keys={['oauth']} size="large" {onBeforeSave}>
{#snippet child({ disabled, config, configToEdit })}
<Alert color="warning">
<div>test</div>
<div class="flex flex-col gap-2 w-full">
<Text size="small">{$t('admin.unlink_all_oauth_accounts_description')}</Text>
<div class="flex justify-end">
<Button size="small" variant="outline" color="warning" onclick={handleUnlinkAllOAuthAccounts}
>{$t('admin.unlink_all_oauth_accounts')}</Button
>
</div>
</div>
</Alert>
<Text>
<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>
<Field label={$t('admin.oauth_enable_description')} {disabled}>
<Switch bind:checked={configToEdit.oauth.enabled} />
</Field>
<Field label="ISSUER_URL" required disabled={disabled || !configToEdit.oauth.enabled}>
<Input bind:value={configToEdit.oauth.issuerUrl} />
</Field>
<Field label="CLIENT_ID" required disabled={disabled || !configToEdit.oauth.enabled}>
<Input bind:value={configToEdit.oauth.clientId} />
</Field>
<Field label="CLIENT_SECRET" required disabled={disabled || !configToEdit.oauth.enabled}>
<Input bind:value={configToEdit.oauth.clientSecret} />
</Field>
{#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}
<Field label="SCOPE" required disabled={disabled || !configToEdit.oauth.enabled}>
<Input bind:value={configToEdit.oauth.scope} />
</Field>
<Field label="ID_TOKEN_SIGNED_RESPONSE_ALG" required disabled={disabled || !configToEdit.oauth.enabled}>
<Input bind:value={configToEdit.oauth.signingAlgorithm} />
</Field>
<Field label="USERINFO_SIGNED_RESPONSE_ALG" required disabled={disabled || !configToEdit.oauth.enabled}>
<Input bind:value={configToEdit.oauth.profileSigningAlgorithm} />
</Field>
<Field
label={$t('admin.oauth_timeout')}
description={$t('admin.oauth_timeout_description')}
required
disabled={disabled || !configToEdit.oauth.enabled}
>
<NumberInput bind:value={configToEdit.oauth.timeout} />
</Field>
<Field
label={$t('admin.oauth_storage_label_claim')}
description={$t('admin.oauth_storage_label_claim_description')}
required
disabled={disabled || !configToEdit.oauth.enabled}
>
<Input bind:value={configToEdit.oauth.storageLabelClaim} />
</Field>
<Field
label={$t('admin.oauth_role_claim')}
description={$t('admin.oauth_role_claim_description')}
required
disabled={disabled || !configToEdit.oauth.enabled}
>
<Input bind:value={configToEdit.oauth.roleClaim} />
</Field>
<Field
label={$t('admin.oauth_storage_quota_claim')}
description={$t('admin.oauth_storage_quota_claim_description')}
required
disabled={disabled || !configToEdit.oauth.enabled}
>
<Input bind:value={configToEdit.oauth.storageQuotaClaim} />
</Field>
<Field
label={$t('admin.oauth_storage_quota_default')}
description={$t('admin.oauth_storage_quota_default_description')}
disabled={disabled || !configToEdit.oauth.enabled}
>
<NumberInput
bind:value={
() => configToEdit.oauth.defaultStorageQuota ?? undefined,
(value) => (configToEdit.oauth.defaultStorageQuota = value ?? null)
}
/>
</Field>
<Field label={$t('admin.oauth_button_text')} disabled={disabled || !configToEdit.oauth.enabled}>
<Input bind:value={configToEdit.oauth.buttonText} />
</Field>
<Field
label={$t('admin.oauth_auto_register')}
description={$t('admin.oauth_auto_register_description')}
disabled={disabled || !configToEdit.oauth.enabled}
>
<Switch bind:checked={configToEdit.oauth.autoRegister} />
</Field>
<Field
label={$t('admin.oauth_auto_launch')}
description={$t('admin.oauth_auto_launch_description')}
disabled={disabled || !configToEdit.oauth.enabled}
>
<Switch bind:checked={configToEdit.oauth.autoLaunch} />
</Field>
<Field
label={$t('admin.oauth_mobile_redirect_uri_override')}
description={$t('admin.oauth_mobile_redirect_uri_override_description', {
values: { callback: 'app.immich:///oauth-callback' },
})}
disabled={disabled || !configToEdit.oauth.enabled}
>
<Switch
bind:checked={configToEdit.oauth.mobileOverrideEnabled}
onCheckedChange={() => handleToggleOverride(configToEdit)}
/>
</Field>
{#if configToEdit.oauth.mobileOverrideEnabled}
<Field label={$t('admin.oauth_mobile_redirect_uri')} required disabled={disabled || !configToEdit.oauth.enabled}>
<Input bind:value={configToEdit.oauth.mobileRedirectUri} />
</Field>
{/if}
{/snippet}
</SystemSettingsModal>

View File

@ -0,0 +1,14 @@
<script lang="ts">
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
import { onBeforeSave } from '$lib/services/system-config.service';
import { Field, Switch } from '@immich/ui';
import { t } from 'svelte-i18n';
</script>
<SystemSettingsModal keys={['passwordLogin']} size="small" {onBeforeSave}>
{#snippet child({ disabled, configToEdit })}
<Field label={$t('admin.password_enable_description')} {disabled}>
<Switch bind:checked={configToEdit.passwordLogin.enabled} />
</Field>
{/snippet}
</SystemSettingsModal>

View File

@ -3,18 +3,18 @@
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import { SettingInputFieldType } from '$lib/constants';
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
import { Field, Input } from '@immich/ui';
import { t } from 'svelte-i18n';
</script>
<SystemSettingsModal keys={['server']}>
{#snippet child({ disabled, config, configToEdit })}
<SettingInputField
inputType={SettingInputFieldType.TEXT}
<Field
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}
/>
>
<Input bind:value={configToEdit.server.externalDomain} />
</Field>
<SettingInputField
inputType={SettingInputFieldType.TEXT}