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

View File

@ -717,8 +717,8 @@ importers:
specifier: file:../open-api/typescript-sdk specifier: file:../open-api/typescript-sdk
version: link:../open-api/typescript-sdk version: link:../open-api/typescript-sdk
'@immich/ui': '@immich/ui':
specifier: ^0.50.0 specifier: ^0.50.1
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) 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': '@mapbox/mapbox-gl-rtl-text':
specifier: 0.2.3 specifier: 0.2.3
version: 0.2.3(mapbox-gl@1.13.3) version: 0.2.3(mapbox-gl@1.13.3)
@ -2989,8 +2989,8 @@ packages:
peerDependencies: peerDependencies:
svelte: ^5.0.0 svelte: ^5.0.0
'@immich/ui@0.50.0': '@immich/ui@0.50.1':
resolution: {integrity: sha512-7AW9SRZTAgal8xlkUAxm7o4+pSG7HcKb+Bh9JpWLaDRRdGyPCZMmsNa9CjZglOQ7wkAD07tQ9u4+zezBLe0dlQ==} resolution: {integrity: sha512-fNlQGh75ZFa/UZAgJaYk9/ItHOXHNNzN4CunjCmE7WocVVkUZbUxopN9Ku3F5GULSqD/zJ5gNO6PQAZ1ZoSaaQ==}
peerDependencies: peerDependencies:
svelte: ^5.0.0 svelte: ^5.0.0
@ -14700,7 +14700,7 @@ snapshots:
dependencies: dependencies:
svelte: 5.45.2 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: dependencies:
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.45.2) '@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.45.2)
'@internationalized/date': 3.10.0 '@internationalized/date': 3.10.0

View File

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

View File

@ -4,7 +4,7 @@
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { systemConfigManager } from '$lib/managers/system-config-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 { SystemConfigContext } from '$lib/types';
import type { SystemConfigDto } from '@immich/sdk'; import type { SystemConfigDto } from '@immich/sdk';
import { Button, FormModal, type ModalSize } from '@immich/ui'; import { Button, FormModal, type ModalSize } from '@immich/ui';
@ -19,13 +19,13 @@
child: Snippet<[SystemConfigContext]>; 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 disabled = $derived(featureFlagsManager.value.configFile);
const config = $derived(systemConfigManager.value); const config = $derived(systemConfigManager.value);
let configToEdit = $state(systemConfigManager.cloneValue()); let configToEdit = $state(systemConfigManager.cloneValue());
const { settings } = $derived(getSystemConfigActions($t, featureFlagsManager.value, systemConfigManager.value)); 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 showResetToDefault = $derived(!isEqual(pick(configToEdit, keys), pick(systemConfigManager.defaultValue, keys)));
const handleResetToDefault = () => { const handleResetToDefault = () => {
@ -47,14 +47,7 @@
</script> </script>
{#if setting} {#if setting}
<FormModal <FormModal {size} title={setting.title} icon={setting.icon} preventDefault {onClose} {onSubmit}>
size={size as 'small' | 'medium'}
title={setting.title}
icon={setting.icon}
preventDefault
{onClose}
{onSubmit}
>
<div class="flex flex-col gap-5"> <div class="flex flex-col gap-5">
{@render child({ disabled, config, configToEdit })} {@render child({ disabled, config, configToEdit })}
{#if showResetToDefault} {#if showResetToDefault}

View File

@ -1,12 +1,14 @@
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { downloadManager } from '$lib/managers/download-manager.svelte'; import { downloadManager } from '$lib/managers/download-manager.svelte';
import { eventManager } from '$lib/managers/event-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 { copyToClipboard } from '$lib/utils';
import { downloadBlob } from '$lib/utils/asset-utils'; import { downloadBlob } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { getFormatter } from '$lib/utils/i18n'; import { getFormatter } from '$lib/utils/i18n';
import { getConfig, updateConfig, type ServerFeaturesDto, type SystemConfigDto } from '@immich/sdk'; import { getConfig, unlinkAllOAuthAccountsAdmin, updateConfig, type ServerFeaturesDto, type SystemConfigDto } from '@immich/sdk';
import { toastManager, type ActionItem } from '@immich/ui'; import { modalManager, toastManager, type ActionItem } from '@immich/ui';
import { import {
mdiAccountOutline, mdiAccountOutline,
mdiBackupRestore, mdiBackupRestore,
@ -18,76 +20,86 @@ import {
mdiDownload, mdiDownload,
mdiFileDocumentOutline, mdiFileDocumentOutline,
mdiFolderOutline, mdiFolderOutline,
mdiImageOutline,
mdiLockOutline, mdiLockOutline,
mdiMapMarkerOutline, mdiMapMarkerOutline,
mdiPaletteOutline, mdiPaletteOutline,
mdiRestore, mdiRestart,
mdiRobotOutline, mdiRobotOutline,
mdiServerOutline, mdiServerOutline,
mdiSync, mdiSync,
mdiTrashCanOutline, mdiTrashCanOutline,
mdiUpdate, mdiUpdate,
mdiUpload, mdiUpload,
mdiVideoOutline, mdiVideoOutline
} from '@mdi/js'; } from '@mdi/js';
import { isEqual } from 'lodash-es'; import { isEqual } from 'lodash-es';
import type { MessageFormatter } from 'svelte-i18n'; 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 = ( export const getSystemConfigActions = (
$t: MessageFormatter, $t: MessageFormatter,
featureFlags: ServerFeaturesDto, featureFlags: ServerFeaturesDto,
config: SystemConfigDto, config: SystemConfigDto,
) => { ) => {
const settings: Array<{ title: string; subtitle: string; href: string; icon: string }> = [ const settings: SettingsGroup[] = [
{ {
title: $t('admin.authentication_settings'), title: $t('admin.authentication_settings'),
subtitle: $t('admin.authentication_settings_description'), 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, icon: mdiLockOutline,
}, },
{ {
title: $t('admin.backup_settings'),
subtitle: $t('admin.backup_settings_description'), title:$t('admin.oauth_settings'), subtitle:$t('admin.oauth_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/backup`, href: `${AppRoute.ADMIN_SETTINGS}/oauth`,
icon: mdiBackupRestore, icon: mdiFileDocumentOutline,
},
]
}, },
{ {
title: $t('admin.image_settings'), title: 'General', items: [
subtitle: $t('admin.image_settings_description'), // {
href: `${AppRoute.ADMIN_SETTINGS}/image`, // title: $t('admin.image_settings'),
icon: mdiImageOutline, // 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'), title: $t('admin.library_settings'),
subtitle: $t('admin.library_settings_description'), subtitle: $t('admin.library_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/library`, href: `${AppRoute.ADMIN_SETTINGS}/library`,
icon: mdiBookshelf, icon: mdiBookshelf,
}, },
{ // {
title: $t('admin.logging_settings'), // title: $t('admin.maintenance_settings'),
subtitle: $t('admin.manage_log_settings'), // subtitle: $t('admin.maintenance_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/logging`, // href: `${AppRoute.ADMIN_SETTINGS}/maintenance`,
icon: mdiFileDocumentOutline, // icon: mdiRestore,
}, // },
{
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'), title: $t('admin.map_gps_settings'),
subtitle: $t('admin.map_gps_settings_description'), subtitle: $t('admin.map_gps_settings_description'),
@ -100,24 +112,12 @@ export const getSystemConfigActions = (
href: `${AppRoute.ADMIN_SETTINGS}/metadata`, href: `${AppRoute.ADMIN_SETTINGS}/metadata`,
icon: mdiDatabaseOutline, 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'), title: $t('admin.notification_settings'),
subtitle: $t('admin.notification_settings_description'), subtitle: $t('admin.notification_settings_description'),
href: `${AppRoute.ADMIN_SETTINGS}/notifications`, href: `${AppRoute.ADMIN_SETTINGS}/notifications`,
icon: mdiBellOutline, 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'), title: $t('admin.storage_template_settings'),
subtitle: $t('admin.storage_template_settings_description'), subtitle: $t('admin.storage_template_settings_description'),
@ -142,11 +142,63 @@ export const getSystemConfigActions = (
href: `${AppRoute.ADMIN_SETTINGS}/user`, href: `${AppRoute.ADMIN_SETTINGS}/user`,
icon: mdiAccountOutline, icon: mdiAccountOutline,
}, },
]
},
{ {
title: $t('admin.version_check_settings'), title: 'Image', items: [
subtitle: $t('admin.version_check_settings_description'), {
href: `${AppRoute.ADMIN_SETTINGS}/version-check`, title: 'General settings',
icon: mdiUpdate, 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'), title: $t('admin.transcoding_settings'),
@ -154,6 +206,108 @@ export const getSystemConfigActions = (
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`, href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
icon: mdiVideoOutline, 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 = { const CopyToClipboard: ActionItem = {
@ -255,3 +409,38 @@ export const handleUploadConfig = () => {
}); });
input.remove(); 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 { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import { systemConfigManager } from '$lib/managers/system-config-manager.svelte'; import { systemConfigManager } from '$lib/managers/system-config-manager.svelte';
import { getSystemConfigActions } from '$lib/services/system-config.service'; 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 { mdiPencilOutline } from '@mdi/js';
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@ -22,39 +22,69 @@
); );
let searchQuery = $state(''); let searchQuery = $state('');
let filteredSettings = $derived( const filteredGroups = $derived(
settings.filter(({ title, subtitle }) => { settings
.map(({ title, items }) => {
const query = searchQuery.toLowerCase(); 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> </script>
<CommandPaletteContext commands={[CopyToClipboard, Upload, Download]} /> <CommandPaletteContext commands={[CopyToClipboard, Upload, Download]} />
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]} actions={[CopyToClipboard, Download, Upload]}> <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="flex place-content-center sm:px-4 mt-4">
<section class="w-full pb-28 sm:w-5/6 md:w-4xl"> <section class="w-full pb-28">
<Container size="medium" center>
{#if featureFlagsManager.value.configFile} {#if featureFlagsManager.value.configFile}
<Alert color="warning" class="text-dark my-4" title={$t('admin.config_set_by_file')} /> <Alert color="warning" class="text-dark my-4" title={$t('admin.config_set_by_file')} />
{/if} {/if}
<div class="mb-4"> <div class="mb-4">
<SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} /> <SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} />
</div> </div>
<div class="flex flex-col gap-2">
{#each filteredSettings as { title, subtitle, href, icon } (href)} <div class="flex flex-col gap-8">
<Button variant="outline" color="secondary" class="flex justify-between border-subtle" {href}> {#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"> <div class="flex flex-col items-start">
<Text size="large" fontWeight="semi-bold" color="primary" class="flex items-center gap-2"> <Text fontWeight="semi-bold" class="flex items-center gap-2">
<Icon {icon} /> <!-- <Icon {icon} /> -->
{title} {title}
</Text> </Text>
<Text>{subtitle}</Text> <Text class="line-clamp-1" color="muted">{subtitle}</Text>
</div> </div>
<Icon icon={mdiPencilOutline} size="1.5rem" /> <Icon icon={mdiPencilOutline} size="1.25rem" />
</Button> </Button>
{/each} {/each}
</div> </div>
</Card>
</div>
{/each}
</div>
</Container>
</section> </section>
</section> </section>
</AdminPageLayout> </AdminPageLayout>

View File

@ -1,16 +1,13 @@
<script lang="ts"> <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 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 SystemSettingsCard from '$lib/components/SystemSettingsCard.svelte';
import { SettingInputFieldType } from '$lib/constants';
import FormatMessage from '$lib/elements/FormatMessage.svelte'; import FormatMessage from '$lib/elements/FormatMessage.svelte';
import AuthDisableLoginConfirmModal from '$lib/modals/AuthDisableLoginConfirmModal.svelte'; import AuthDisableLoginConfirmModal from '$lib/modals/AuthDisableLoginConfirmModal.svelte';
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte'; import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
import type { SystemConfigContext } from '$lib/types'; import type { SystemConfigContext } from '$lib/types';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { OAuthTokenEndpointAuthMethod, unlinkAllOAuthAccountsAdmin, type SystemConfigDto } from '@immich/sdk'; import { OAuthTokenEndpointAuthMethod, unlinkAllOAuthAccountsAdmin, 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 { mdiRestart } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
@ -58,11 +55,9 @@
<SystemSettingsModal keys={['passwordLogin', 'oauth']} size="large" {onBeforeSave}> <SystemSettingsModal keys={['passwordLogin', 'oauth']} size="large" {onBeforeSave}>
{#snippet child({ disabled, config, configToEdit })} {#snippet child({ disabled, config, configToEdit })}
<SystemSettingsCard title={$t('admin.password_settings')} subtitle={$t('admin.password_settings_description')}> <SystemSettingsCard title={$t('admin.password_settings')} subtitle={$t('admin.password_settings_description')}>
<SettingSwitch <Field label={$t('admin.password_enable_description')} {disabled}>
title={$t('admin.password_enable_description')} <Switch bind:checked={configToEdit.passwordLogin.enabled} />
{disabled} </Field>
bind:checked={configToEdit.passwordLogin.enabled}
/>
</SystemSettingsCard> </SystemSettingsCard>
<SystemSettingsCard title={$t('admin.oauth_settings')} subtitle={$t('admin.oauth_settings_description')}> <SystemSettingsCard title={$t('admin.oauth_settings')} subtitle={$t('admin.oauth_settings_description')}>
@ -76,11 +71,9 @@
</FormatMessage> </FormatMessage>
</Text> </Text>
<SettingSwitch <Field label={$t('admin.oauth_enable_description')} {disabled}>
{disabled} <Switch bind:checked={configToEdit.oauth.enabled} />
title={$t('admin.oauth_enable_description')} </Field>
bind:checked={configToEdit.oauth.enabled}
/>
<div class="flex flex-col gap-2"> <div class="flex flex-col gap-2">
<Text size="small">{$t('admin.unlink_all_oauth_accounts_description')}</Text> <Text size="small">{$t('admin.unlink_all_oauth_accounts_description')}</Text>
@ -91,32 +84,17 @@
</div> </div>
</div> </div>
<SettingInputField <Field label="ISSUER_URL" required disabled={disabled || !configToEdit.oauth.enabled}>
inputType={SettingInputFieldType.TEXT} <Input bind:value={configToEdit.oauth.issuerUrl} />
label="ISSUER_URL" </Field>
bind:value={configToEdit.oauth.issuerUrl}
required={true}
disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(configToEdit.oauth.issuerUrl === config.oauth.issuerUrl)}
/>
<SettingInputField <Field label="CLIENT_ID" required disabled={disabled || !configToEdit.oauth.enabled}>
inputType={SettingInputFieldType.TEXT} <Input bind:value={configToEdit.oauth.clientId} />
label="CLIENT_ID" </Field>
bind:value={configToEdit.oauth.clientId}
required={true}
disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(configToEdit.oauth.clientId === config.oauth.clientId)}
/>
<SettingInputField <Field label="CLIENT_SECRET" required disabled={disabled || !configToEdit.oauth.enabled}>
inputType={SettingInputFieldType.TEXT} <Input bind:value={configToEdit.oauth.clientSecret} />
label="CLIENT_SECRET" </Field>
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} {#if configToEdit.oauth.clientSecret}
<SettingSelect <SettingSelect
@ -132,125 +110,108 @@
/> />
{/if} {/if}
<SettingInputField <Field label="SCOPE" required disabled={disabled || !configToEdit.oauth.enabled}>
inputType={SettingInputFieldType.TEXT} <Input bind:value={configToEdit.oauth.scope} />
label="SCOPE" </Field>
bind:value={configToEdit.oauth.scope}
required={true}
disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(configToEdit.oauth.scope === config.oauth.scope)}
/>
<SettingInputField <Field label="ID_TOKEN_SIGNED_RESPONSE_ALG" required disabled={disabled || !configToEdit.oauth.enabled}>
inputType={SettingInputFieldType.TEXT} <Input bind:value={configToEdit.oauth.signingAlgorithm} />
label="ID_TOKEN_SIGNED_RESPONSE_ALG" </Field>
bind:value={configToEdit.oauth.signingAlgorithm}
required={true}
disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(configToEdit.oauth.signingAlgorithm === config.oauth.signingAlgorithm)}
/>
<SettingInputField <Field label="USERINFO_SIGNED_RESPONSE_ALG" required disabled={disabled || !configToEdit.oauth.enabled}>
inputType={SettingInputFieldType.TEXT} <Input bind:value={configToEdit.oauth.profileSigningAlgorithm} />
label="USERINFO_SIGNED_RESPONSE_ALG" </Field>
bind:value={configToEdit.oauth.profileSigningAlgorithm}
required={true}
disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(configToEdit.oauth.profileSigningAlgorithm === config.oauth.profileSigningAlgorithm)}
/>
<SettingInputField <Field
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.oauth_timeout')} label={$t('admin.oauth_timeout')}
description={$t('admin.oauth_timeout_description')} description={$t('admin.oauth_timeout_description')}
required={true} required
bind:value={configToEdit.oauth.timeout}
disabled={disabled || !configToEdit.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(configToEdit.oauth.timeout === config.oauth.timeout)} >
/> <NumberInput bind:value={configToEdit.oauth.timeout} />
</Field>
<SettingInputField <Field
inputType={SettingInputFieldType.TEXT}
label={$t('admin.oauth_storage_label_claim')} label={$t('admin.oauth_storage_label_claim')}
description={$t('admin.oauth_storage_label_claim_description')} description={$t('admin.oauth_storage_label_claim_description')}
bind:value={configToEdit.oauth.storageLabelClaim} required
required={true}
disabled={disabled || !configToEdit.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(configToEdit.oauth.storageLabelClaim === config.oauth.storageLabelClaim)} >
/> <Input bind:value={configToEdit.oauth.storageLabelClaim} />
</Field>
<SettingInputField <Field
inputType={SettingInputFieldType.TEXT}
label={$t('admin.oauth_role_claim')} label={$t('admin.oauth_role_claim')}
description={$t('admin.oauth_role_claim_description')} description={$t('admin.oauth_role_claim_description')}
bind:value={configToEdit.oauth.roleClaim} required
required={true}
disabled={disabled || !configToEdit.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(configToEdit.oauth.roleClaim === config.oauth.roleClaim)} >
/> <Input bind:value={configToEdit.oauth.roleClaim} />
</Field>
<SettingInputField <Field
inputType={SettingInputFieldType.TEXT}
label={$t('admin.oauth_storage_quota_claim')} label={$t('admin.oauth_storage_quota_claim')}
description={$t('admin.oauth_storage_quota_claim_description')} description={$t('admin.oauth_storage_quota_claim_description')}
bind:value={configToEdit.oauth.storageQuotaClaim} required
required={true}
disabled={disabled || !configToEdit.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(configToEdit.oauth.storageQuotaClaim === config.oauth.storageQuotaClaim)} >
/> <Input bind:value={configToEdit.oauth.storageQuotaClaim} />
</Field>
<SettingInputField <Field
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.oauth_storage_quota_default')} label={$t('admin.oauth_storage_quota_default')}
description={$t('admin.oauth_storage_quota_default_description')} description={$t('admin.oauth_storage_quota_default_description')}
bind:value={configToEdit.oauth.defaultStorageQuota}
required={false}
disabled={disabled || !configToEdit.oauth.enabled} 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 <Field label={$t('admin.oauth_button_text')} disabled={disabled || !configToEdit.oauth.enabled}>
inputType={SettingInputFieldType.TEXT} <Input bind:value={configToEdit.oauth.buttonText} />
label={$t('admin.oauth_button_text')} </Field>
bind:value={configToEdit.oauth.buttonText}
required={false} <Field
label={$t('admin.oauth_auto_register')}
description={$t('admin.oauth_auto_register_description')}
disabled={disabled || !configToEdit.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(configToEdit.oauth.buttonText === config.oauth.buttonText)} >
/> <Switch bind:checked={configToEdit.oauth.autoRegister} />
</Field>
<SettingSwitch <Field
title={$t('admin.oauth_auto_register')} label={$t('admin.oauth_auto_launch')}
subtitle={$t('admin.oauth_auto_register_description')} description={$t('admin.oauth_auto_launch_description')}
bind:checked={configToEdit.oauth.autoRegister}
disabled={disabled || !configToEdit.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
/> >
<Switch bind:checked={configToEdit.oauth.autoLaunch} />
</Field>
<SettingSwitch <Field
title={$t('admin.oauth_auto_launch')} label={$t('admin.oauth_mobile_redirect_uri_override')}
subtitle={$t('admin.oauth_auto_launch_description')} description={$t('admin.oauth_mobile_redirect_uri_override_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' }, values: { callback: 'app.immich:///oauth-callback' },
})} })}
disabled={disabled || !configToEdit.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
onToggle={() => handleToggleOverride(configToEdit)} >
<Switch
bind:checked={configToEdit.oauth.mobileOverrideEnabled} bind:checked={configToEdit.oauth.mobileOverrideEnabled}
onCheckedChange={() => handleToggleOverride(configToEdit)}
/> />
</Field>
{#if configToEdit.oauth.mobileOverrideEnabled} {#if configToEdit.oauth.mobileOverrideEnabled}
<SettingInputField <Field
inputType={SettingInputFieldType.TEXT}
label={$t('admin.oauth_mobile_redirect_uri')} label={$t('admin.oauth_mobile_redirect_uri')}
bind:value={configToEdit.oauth.mobileRedirectUri} required
required={true}
disabled={disabled || !configToEdit.oauth.enabled} disabled={disabled || !configToEdit.oauth.enabled}
isEdited={!(configToEdit.oauth.mobileRedirectUri === config.oauth.mobileRedirectUri)} >
/> <Input bind:value={configToEdit.oauth.mobileRedirectUri} />
</Field>
{/if} {/if}
</SystemSettingsCard> </SystemSettingsCard>
{/snippet} {/snippet}

View File

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

View File

@ -1,9 +1,8 @@
<script lang="ts"> <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 SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
import { getQueueName } from '$lib/utils'; import { getQueueName } from '$lib/utils';
import { QueueName, type SystemConfigDto, type SystemConfigJobDto } from '@immich/sdk'; import { QueueName, type SystemConfigDto, type SystemConfigJobDto } from '@immich/sdk';
import { Field, HelperText, NumberInput } from '@immich/ui';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
const queueNames = [ const queueNames = [
@ -29,30 +28,23 @@
</script> </script>
<SystemSettingsModal keys={['user']}> <SystemSettingsModal keys={['user']}>
{#snippet child({ disabled, config, configToEdit })} {#snippet child({ disabled, configToEdit })}
{#each queueNames as queueName (queueName)} {#each queueNames as queueName (queueName)}
<div class="ms-4 mt-4 flex flex-col gap-4">
{#if isSystemConfigJobDto(configToEdit, queueName)} {#if isSystemConfigJobDto(configToEdit, queueName)}
<SettingInputField <Field
inputType={SettingInputFieldType.NUMBER} required
{disabled} {disabled}
label={$t('admin.job_concurrency', { values: { job: $getQueueName(queueName) } })} label={$t('admin.job_concurrency', { values: { job: $getQueueName(queueName) } })}
description="" description=""
bind:value={configToEdit.job[queueName].concurrency} >
required={true} <NumberInput bind:value={configToEdit.job[queueName].concurrency} />
isEdited={!(configToEdit.job[queueName].concurrency == config.job[queueName].concurrency)} </Field>
/>
{:else} {:else}
<SettingInputField <Field label={$t('admin.job_concurrency', { values: { job: $getQueueName(queueName) } })}>
inputType={SettingInputFieldType.NUMBER} <NumberInput value={1} disabled={true} />
label={$t('admin.job_concurrency', { values: { job: $getQueueName(queueName) } })} <HelperText>{$t('admin.job_not_concurrency_safe')}</HelperText>
description="" </Field>
value={1}
disabled={true}
title={$t('admin.job_not_concurrency_safe')}
/>
{/if} {/if}
</div>
{/each} {/each}
{/snippet} {/snippet}
</SystemSettingsModal> </SystemSettingsModal>

View File

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

View File

@ -1,18 +1,17 @@
<script lang="ts"> <script lang="ts">
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte'; import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte'; import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
import { LogLevel } from '@immich/sdk'; import { LogLevel } from '@immich/sdk';
import { Field, Switch } from '@immich/ui';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
</script> </script>
<SystemSettingsModal keys={['logging']}> <SystemSettingsModal keys={['logging']}>
{#snippet child({ disabled, config, configToEdit })} {#snippet child({ disabled, config, configToEdit })}
<SettingSwitch <Field required {disabled} label={$t('admin.logging_enable_description')}>
title={$t('admin.logging_enable_description')} <Switch bind:checked={configToEdit.logging.enabled} />
{disabled} </Field>
bind:checked={configToEdit.logging.enabled}
/>
<SettingSelect <SettingSelect
label={$t('level')} label={$t('level')}
desc={$t('admin.logging_level_description')} desc={$t('admin.logging_level_description')}

View File

@ -1,52 +1,59 @@
<script lang="ts"> <script lang="ts">
import SystemSettingsCard from '$lib/components/SystemSettingsCard.svelte'; 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 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 FormatMessage from '$lib/elements/FormatMessage.svelte';
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.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 { mdiPlus, mdiTrashCanOutline } from '@mdi/js';
import { isEqual } from 'lodash-es';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
</script> </script>
<SystemSettingsModal keys={['machineLearning']} size="large"> <SystemSettingsModal keys={['machineLearning']} size="large">
{#snippet child({ disabled, config, configToEdit })} {#snippet child({ disabled, config, configToEdit })}
<div class="flex flex-col gap-4"> <Field
<SettingSwitch label={$t('admin.machine_learning_enabled')}
title={$t('admin.machine_learning_enabled')} description={$t('admin.machine_learning_enabled_description')}
subtitle={$t('admin.machine_learning_enabled_description')}
{disabled} {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} {#if configToEdit.machineLearning.urls.length > 1}
<IconButton <IconButton
aria-label="" size="small"
aria-label={$t('remove')}
onclick={() => configToEdit.machineLearning.urls.splice(i, 1)} onclick={() => configToEdit.machineLearning.urls.splice(i, 1)}
icon={mdiTrashCanOutline} icon={mdiTrashCanOutline}
color="danger" color="danger"
/> />
{/if} {/if}
{/snippet} {/snippet}
</SettingInputField> </Input>
{/each} {/each}
</div> </Stack>
<div class="flex justify-end"> <div class="flex justify-end">
<Button <Button
@ -63,103 +70,99 @@
title={$t('admin.machine_learning_availability_checks')} title={$t('admin.machine_learning_availability_checks')}
subtitle={$t('admin.machine_learning_availability_checks_description')} subtitle={$t('admin.machine_learning_availability_checks_description')}
> >
<SettingSwitch <Field
title={$t('admin.machine_learning_availability_checks_enabled')} label={$t('admin.machine_learning_availability_checks_enabled')}
bind:checked={configToEdit.machineLearning.availabilityChecks.enabled}
disabled={disabled || !configToEdit.machineLearning.enabled} disabled={disabled || !configToEdit.machineLearning.enabled}
/> >
<Switch bind:checked={configToEdit.machineLearning.availabilityChecks.enabled} />
</Field>
<SettingInputField <Field
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_availability_checks_interval')} label={$t('admin.machine_learning_availability_checks_interval')}
bind:value={configToEdit.machineLearning.availabilityChecks.interval}
description={$t('admin.machine_learning_availability_checks_interval_description')} description={$t('admin.machine_learning_availability_checks_interval_description')}
disabled={disabled || disabled={disabled ||
!configToEdit.machineLearning.enabled || !configToEdit.machineLearning.enabled ||
!configToEdit.machineLearning.availabilityChecks.enabled} !configToEdit.machineLearning.availabilityChecks.enabled}
isEdited={configToEdit.machineLearning.availabilityChecks.interval !== >
config.machineLearning.availabilityChecks.interval} <NumberInput bind:value={configToEdit.machineLearning.availabilityChecks.interval} />
/> </Field>
<SettingInputField <Field
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_availability_checks_timeout')} label={$t('admin.machine_learning_availability_checks_timeout')}
bind:value={configToEdit.machineLearning.availabilityChecks.timeout}
description={$t('admin.machine_learning_availability_checks_timeout_description')} description={$t('admin.machine_learning_availability_checks_timeout_description')}
disabled={disabled || disabled={disabled ||
!configToEdit.machineLearning.enabled || !configToEdit.machineLearning.enabled ||
!configToEdit.machineLearning.availabilityChecks.enabled} !configToEdit.machineLearning.availabilityChecks.enabled}
isEdited={configToEdit.machineLearning.availabilityChecks.timeout !== >
config.machineLearning.availabilityChecks.timeout} <NumberInput bind:value={configToEdit.machineLearning.availabilityChecks.timeout} />
/> </Field>
</SystemSettingsCard> </SystemSettingsCard>
<SystemSettingsCard <SystemSettingsCard
title={$t('admin.machine_learning_smart_search')} title={$t('admin.machine_learning_smart_search')}
subtitle={$t('admin.machine_learning_smart_search_description')} subtitle={$t('admin.machine_learning_smart_search_description')}
> >
<SettingSwitch <Field
title={$t('admin.machine_learning_smart_search_enabled')} label={$t('admin.machine_learning_smart_search_enabled')}
subtitle={$t('admin.machine_learning_smart_search_enabled_description')} description={$t('admin.machine_learning_smart_search_enabled_description')}
bind:checked={configToEdit.machineLearning.clip.enabled}
disabled={disabled || !configToEdit.machineLearning.enabled} disabled={disabled || !configToEdit.machineLearning.enabled}
/> >
<Switch bind:checked={configToEdit.machineLearning.clip.enabled} />
</Field>
<SettingInputField <Field
inputType={SettingInputFieldType.TEXT}
label={$t('admin.machine_learning_clip_model')} label={$t('admin.machine_learning_clip_model')}
bind:value={configToEdit.machineLearning.clip.modelName}
required={true} required={true}
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.clip.enabled} disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.clip.enabled}
isEdited={configToEdit.machineLearning.clip.modelName !== config.machineLearning.clip.modelName}
> >
{#snippet descriptionSnippet()} <Input bind:value={configToEdit.machineLearning.clip.modelName} />
<p class="immich-form-label pb-2 text-sm"> <HelperText>
<FormatMessage key="admin.machine_learning_clip_model_description"> <FormatMessage key="admin.machine_learning_clip_model_description">
{#snippet children({ message })} {#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} {/snippet}
</FormatMessage> </FormatMessage>
</p> </HelperText>
{/snippet} </Field>
</SettingInputField>
</SystemSettingsCard> </SystemSettingsCard>
<SystemSettingsCard <SystemSettingsCard
title={$t('admin.machine_learning_duplicate_detection')} title={$t('admin.machine_learning_duplicate_detection')}
subtitle={$t('admin.machine_learning_duplicate_detection_setting_description')} subtitle={$t('admin.machine_learning_duplicate_detection_setting_description')}
> >
<SettingSwitch <Field
title={$t('admin.machine_learning_duplicate_detection_enabled')} label={$t('admin.machine_learning_duplicate_detection_enabled')}
subtitle={$t('admin.machine_learning_duplicate_detection_enabled_description')} description={$t('admin.machine_learning_duplicate_detection_enabled_description')}
bind:checked={configToEdit.machineLearning.duplicateDetection.enabled}
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.clip.enabled} disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.clip.enabled}
/> >
<Switch bind:checked={configToEdit.machineLearning.duplicateDetection.enabled} />
</Field>
<SettingInputField <Field
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_max_detection_distance')} 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} bind:value={configToEdit.machineLearning.duplicateDetection.maxDistance}
step="0.0005" step="0.0005"
min={0.001} min={0.001}
max={0.1} max={0.1}
description={$t('admin.machine_learning_max_detection_distance_description')}
disabled={disabled || !featureFlagsManager.value.duplicateDetection}
isEdited={configToEdit.machineLearning.duplicateDetection.maxDistance !==
config.machineLearning.duplicateDetection.maxDistance}
/> />
</Field>
</SystemSettingsCard> </SystemSettingsCard>
<SystemSettingsCard <SystemSettingsCard
title={$t('admin.machine_learning_facial_recognition')} title={$t('admin.machine_learning_facial_recognition')}
subtitle={$t('admin.machine_learning_facial_recognition_description')} subtitle={$t('admin.machine_learning_facial_recognition_description')}
> >
<SettingSwitch <Field
title={$t('admin.machine_learning_facial_recognition_setting')} label={$t('admin.machine_learning_facial_recognition_setting')}
subtitle={$t('admin.machine_learning_facial_recognition_setting_description')} description={$t('admin.machine_learning_facial_recognition_setting_description')}
bind:checked={configToEdit.machineLearning.facialRecognition.enabled}
disabled={disabled || !configToEdit.machineLearning.enabled} disabled={disabled || !configToEdit.machineLearning.enabled}
/> >
<Switch bind:checked={configToEdit.machineLearning.facialRecognition.enabled} />
</Field>
<SettingSelect <SettingSelect
label={$t('admin.machine_learning_facial_recognition_model')} label={$t('admin.machine_learning_facial_recognition_model')}
@ -179,61 +182,57 @@
config.machineLearning.facialRecognition.modelName} config.machineLearning.facialRecognition.modelName}
/> />
<SettingInputField <Field
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_min_detection_score')} label={$t('admin.machine_learning_min_detection_score')}
description={$t('admin.machine_learning_min_detection_score_description')} description={$t('admin.machine_learning_min_detection_score_description')}
disabled={disabled ||
!configToEdit.machineLearning.enabled ||
!configToEdit.machineLearning.facialRecognition.enabled}
>
<NumberInput
bind:value={configToEdit.machineLearning.facialRecognition.minScore} bind:value={configToEdit.machineLearning.facialRecognition.minScore}
step="0.01" step="0.01"
min={0.1} min={0.1}
max={1} max={1}
/>
</Field>
<Field
label={$t('admin.machine_learning_max_recognition_distance')}
description={$t('admin.machine_learning_max_recognition_distance_description')}
disabled={disabled || disabled={disabled ||
!configToEdit.machineLearning.enabled || !configToEdit.machineLearning.enabled ||
!configToEdit.machineLearning.facialRecognition.enabled} !configToEdit.machineLearning.facialRecognition.enabled}
isEdited={configToEdit.machineLearning.facialRecognition.minScore !== >
config.machineLearning.facialRecognition.minScore} <NumberInput
/>
<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} bind:value={configToEdit.machineLearning.facialRecognition.maxDistance}
step="0.01" step="0.01"
min={0.1} min={0.1}
max={2} max={2}
disabled={disabled ||
!configToEdit.machineLearning.enabled ||
!configToEdit.machineLearning.facialRecognition.enabled}
isEdited={configToEdit.machineLearning.facialRecognition.maxDistance !==
config.machineLearning.facialRecognition.maxDistance}
/> />
</Field>
<SettingInputField <Field
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_min_recognized_faces')} label={$t('admin.machine_learning_min_recognized_faces')}
description={$t('admin.machine_learning_min_recognized_faces_description')} description={$t('admin.machine_learning_min_recognized_faces_description')}
bind:value={configToEdit.machineLearning.facialRecognition.minFaces}
step="1"
min={1}
disabled={disabled || disabled={disabled ||
!configToEdit.machineLearning.enabled || !configToEdit.machineLearning.enabled ||
!configToEdit.machineLearning.facialRecognition.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>
<SystemSettingsCard <SystemSettingsCard
title={$t('admin.machine_learning_ocr')} title={$t('admin.machine_learning_ocr')}
subtitle={$t('admin.machine_learning_ocr_description')} subtitle={$t('admin.machine_learning_ocr_description')}
> >
<SettingSwitch <Field
title={$t('admin.machine_learning_ocr_enabled')} label={$t('admin.machine_learning_ocr_enabled')}
subtitle={$t('admin.machine_learning_ocr_enabled_description')} description={$t('admin.machine_learning_ocr_enabled_description')}
bind:checked={configToEdit.machineLearning.ocr.enabled}
disabled={disabled || !configToEdit.machineLearning.enabled} disabled={disabled || !configToEdit.machineLearning.enabled}
/> >
<Switch bind:checked={configToEdit.machineLearning.ocr.enabled} />
</Field>
<SettingSelect <SettingSelect
label={$t('admin.machine_learning_ocr_model')} label={$t('admin.machine_learning_ocr_model')}
@ -254,41 +253,29 @@
isEdited={configToEdit.machineLearning.ocr.modelName !== config.machineLearning.ocr.modelName} isEdited={configToEdit.machineLearning.ocr.modelName !== config.machineLearning.ocr.modelName}
/> />
<SettingInputField <Field
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_ocr_min_detection_score')} label={$t('admin.machine_learning_ocr_min_detection_score')}
description={$t('admin.machine_learning_ocr_min_detection_score_description')} description={$t('admin.machine_learning_ocr_min_detection_score_description')}
bind:value={configToEdit.machineLearning.ocr.minDetectionScore}
step="0.1"
min={0.1}
max={1}
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.ocr.enabled} 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 <Field
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_ocr_min_recognition_score')} label={$t('admin.machine_learning_ocr_min_recognition_score')}
description={$t('admin.machine_learning_ocr_min_score_recognition_description')} description={$t('admin.machine_learning_ocr_min_score_recognition_description')}
bind:value={configToEdit.machineLearning.ocr.minRecognitionScore}
step="0.1"
min={0.1}
max={1}
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.ocr.enabled} 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 <Field
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.machine_learning_ocr_max_resolution')} label={$t('admin.machine_learning_ocr_max_resolution')}
description={$t('admin.machine_learning_ocr_max_resolution_description')} description={$t('admin.machine_learning_ocr_max_resolution_description')}
bind:value={configToEdit.machineLearning.ocr.maxResolution}
min={1}
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.ocr.enabled} 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> </SystemSettingsCard>
</div>
{/snippet} {/snippet}
</SystemSettingsModal> </SystemSettingsModal>

View File

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

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