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