mirror of
https://github.com/immich-app/immich.git
synced 2026-01-26 11:24:44 -08:00
Compare commits
3 Commits
7447d407eb
...
feat/syste
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ef7a37a26a | ||
|
|
009a37f0a7 | ||
|
|
31f2c7b505 |
@@ -78,7 +78,6 @@
|
||||
"exclusion_pattern_description": "Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have folders that contain files you don't want to import, such as RAW files.",
|
||||
"export_config_as_json_description": "Download the current system config as a JSON file",
|
||||
"external_libraries_page_description": "Admin external library page",
|
||||
"external_library_management": "External Library Management",
|
||||
"face_detection": "Face detection",
|
||||
"face_detection_description": "Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. \"Refresh\" (re-)processes all assets. \"Reset\" additionally clears all current face data. \"Missing\" queues assets that haven't been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.",
|
||||
"facial_recognition_job_description": "Group detected faces into people. This step runs after Face Detection is complete. \"Reset\" (re-)clusters all faces. \"Missing\" queues faces that don't have a person assigned.",
|
||||
@@ -273,7 +272,7 @@
|
||||
"oauth_timeout_description": "Timeout for requests in milliseconds",
|
||||
"ocr_job_description": "Use machine learning to recognize text in images",
|
||||
"password_enable_description": "Login with email and password",
|
||||
"password_settings": "Password Login",
|
||||
"password_settings": "Password login",
|
||||
"password_settings_description": "Manage password login settings",
|
||||
"paths_validated_successfully": "All paths validated successfully",
|
||||
"person_cleanup_job": "Person cleanup",
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -717,8 +717,8 @@ importers:
|
||||
specifier: file:../open-api/typescript-sdk
|
||||
version: link:../open-api/typescript-sdk
|
||||
'@immich/ui':
|
||||
specifier: ^0.49.2
|
||||
version: 0.49.3(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)
|
||||
specifier: ^0.50.1
|
||||
version: 0.50.1(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)
|
||||
'@mapbox/mapbox-gl-rtl-text':
|
||||
specifier: 0.2.3
|
||||
version: 0.2.3(mapbox-gl@1.13.3)
|
||||
@@ -2989,8 +2989,8 @@ packages:
|
||||
peerDependencies:
|
||||
svelte: ^5.0.0
|
||||
|
||||
'@immich/ui@0.49.3':
|
||||
resolution: {integrity: sha512-joqT72Y6gmGK6z25Suzr2VhYANrLo43g20T4UHmbQenz/z/Ax6sl1Ao9SjIOwEkKMm9N3Txoh7WOOzmHVl04OA==}
|
||||
'@immich/ui@0.50.1':
|
||||
resolution: {integrity: sha512-fNlQGh75ZFa/UZAgJaYk9/ItHOXHNNzN4CunjCmE7WocVVkUZbUxopN9Ku3F5GULSqD/zJ5gNO6PQAZ1ZoSaaQ==}
|
||||
peerDependencies:
|
||||
svelte: ^5.0.0
|
||||
|
||||
@@ -14700,7 +14700,7 @@ snapshots:
|
||||
dependencies:
|
||||
svelte: 5.45.2
|
||||
|
||||
'@immich/ui@0.49.3(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)':
|
||||
'@immich/ui@0.50.1(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(yaml@2.8.2)))(svelte@5.45.2)':
|
||||
dependencies:
|
||||
'@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.45.2)
|
||||
'@internationalized/date': 3.10.0
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"@formatjs/icu-messageformat-parser": "^2.9.8",
|
||||
"@immich/justified-layout-wasm": "^0.4.3",
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@immich/ui": "^0.49.2",
|
||||
"@immich/ui": "^0.50.1",
|
||||
"@mapbox/mapbox-gl-rtl-text": "0.2.3",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@photo-sphere-viewer/core": "^5.14.0",
|
||||
|
||||
24
web/src/lib/components/HeaderActionButton.svelte
Normal file
24
web/src/lib/components/HeaderActionButton.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import type { HeaderButtonActionItem } from '$lib/types';
|
||||
import { Button } from '@immich/ui';
|
||||
|
||||
type Props = {
|
||||
action: HeaderButtonActionItem;
|
||||
};
|
||||
|
||||
const { action }: Props = $props();
|
||||
const { title, icon, color = 'secondary', onAction } = $derived(action);
|
||||
</script>
|
||||
|
||||
{#if action.$if?.() ?? true}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
{color}
|
||||
leadingIcon={icon}
|
||||
onclick={() => onAction(action)}
|
||||
title={action.data?.title}
|
||||
>
|
||||
{title}
|
||||
</Button>
|
||||
{/if}
|
||||
@@ -1,17 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { type ActionItem, Button, Text } from '@immich/ui';
|
||||
|
||||
type Props = {
|
||||
action: ActionItem;
|
||||
title?: string;
|
||||
};
|
||||
|
||||
const { action, title: titleAttr }: Props = $props();
|
||||
const { title, icon, color = 'secondary', onAction } = $derived(action);
|
||||
</script>
|
||||
|
||||
{#if action.$if?.() ?? true}
|
||||
<Button variant="ghost" size="small" {color} leadingIcon={icon} onclick={() => onAction(action)} title={titleAttr}>
|
||||
<Text class="hidden md:block">{title}</Text>
|
||||
</Button>
|
||||
{/if}
|
||||
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>
|
||||
@@ -1,19 +1,33 @@
|
||||
<script lang="ts">
|
||||
import PageContent from '$lib/components/layouts/PageContent.svelte';
|
||||
import TitleLayout from '$lib/components/layouts/TitleLayout.svelte';
|
||||
import NavigationBar from '$lib/components/shared-components/navigation-bar/navigation-bar.svelte';
|
||||
import AdminSidebar from '$lib/sidebars/AdminSidebar.svelte';
|
||||
import { sidebarStore } from '$lib/stores/sidebar.svelte';
|
||||
import { AppShell, AppShellHeader, AppShellSidebar, Scrollable, type BreadcrumbItem } from '@immich/ui';
|
||||
import type { HeaderButtonActionItem } from '$lib/types';
|
||||
import {
|
||||
AppShell,
|
||||
AppShellHeader,
|
||||
AppShellSidebar,
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
ContextMenuButton,
|
||||
HStack,
|
||||
MenuItemType,
|
||||
Scrollable,
|
||||
isMenuItemType,
|
||||
type BreadcrumbItem,
|
||||
} from '@immich/ui';
|
||||
import { mdiSlashForward } from '@mdi/js';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
type Props = {
|
||||
breadcrumbs: BreadcrumbItem[];
|
||||
buttons?: Snippet;
|
||||
actions?: Array<HeaderButtonActionItem | MenuItemType>;
|
||||
children?: Snippet;
|
||||
};
|
||||
|
||||
let { breadcrumbs, buttons, children }: Props = $props();
|
||||
let { breadcrumbs, actions = [], children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<AppShell>
|
||||
@@ -24,11 +38,37 @@
|
||||
<AdminSidebar />
|
||||
</AppShellSidebar>
|
||||
|
||||
<TitleLayout {breadcrumbs} {buttons}>
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="flex h-16 w-full justify-between items-center border-b py-2 px-4 md:px-2">
|
||||
<Breadcrumbs items={breadcrumbs} separator={mdiSlashForward} />
|
||||
|
||||
{#if actions.length > 0}
|
||||
<div class="hidden md:block">
|
||||
<HStack gap={0}>
|
||||
{#each actions as action, i (i)}
|
||||
{#if !isMenuItemType(action) && (action.$if?.() ?? true)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="small"
|
||||
color={action.color ?? 'secondary'}
|
||||
leadingIcon={action.icon}
|
||||
onclick={() => action.onAction(action)}
|
||||
title={action.data?.title}
|
||||
>
|
||||
{action.title}
|
||||
</Button>
|
||||
{/if}
|
||||
{/each}
|
||||
</HStack>
|
||||
</div>
|
||||
|
||||
<ContextMenuButton aria-label={$t('open')} items={actions} class="md:hidden" />
|
||||
{/if}
|
||||
</div>
|
||||
<Scrollable class="grow">
|
||||
<PageContent>
|
||||
{@render children?.()}
|
||||
</PageContent>
|
||||
</Scrollable>
|
||||
</TitleLayout>
|
||||
</div>
|
||||
</AppShell>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { Breadcrumbs, type BreadcrumbItem } from '@immich/ui';
|
||||
import { mdiSlashForward } from '@mdi/js';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
type Props = {
|
||||
breadcrumbs: BreadcrumbItem[];
|
||||
buttons?: Snippet;
|
||||
children?: Snippet;
|
||||
};
|
||||
|
||||
let { breadcrumbs, buttons, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="flex h-16 w-full place-items-center justify-between border-b p-2">
|
||||
<Breadcrumbs items={breadcrumbs} separator={mdiSlashForward} />
|
||||
{@render buttons?.()}
|
||||
</div>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -19,11 +19,6 @@
|
||||
!isEqual(pick(systemConfigManager.value, keys), pick(systemConfigManager.defaultValue, keys)),
|
||||
);
|
||||
|
||||
const handleReset = () => {
|
||||
configToEdit = systemConfigManager.cloneValue();
|
||||
toastManager.info($t('admin.reset_settings_to_recent_saved'));
|
||||
};
|
||||
|
||||
const handleResetToDefault = () => {
|
||||
const defaultConfig = systemConfigManager.cloneDefaultValue();
|
||||
|
||||
@@ -51,7 +46,7 @@
|
||||
</div>
|
||||
|
||||
<div class="flex gap-1">
|
||||
<Button shape="round" {disabled} size="small" color="secondary" onclick={handleReset}>{$t('reset')}</Button>
|
||||
<Button shape="round" {disabled} size="small" color="secondary" onclick={onCancel}>{$t('cancel')}</Button>
|
||||
<Button shape="round" type="submit" {disabled} size="small" onclick={handleSave}>{$t('save')}</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mb-4 w-full">
|
||||
<div class="w-full">
|
||||
<div class="flex h-6.5 place-items-center gap-1">
|
||||
<label class="font-medium text-primary text-sm" for="{name}-select">
|
||||
{label}
|
||||
|
||||
@@ -79,7 +79,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mb-4 w-full">
|
||||
<div class="w-full">
|
||||
<div class="flex place-items-center gap-1">
|
||||
<label class="font-medium text-primary text-sm min-h-6 uppercase" for={label}>{label}</label>
|
||||
{#if required}
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="mb-4 w-full">
|
||||
<div class="w-full">
|
||||
<div class="flex h-6.5 place-items-center gap-1">
|
||||
<label class="font-medium text-primary text-sm" for="{name}-select">{label}</label>
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
interface Props {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
subtitle?: string | Snippet;
|
||||
checked?: boolean;
|
||||
disabled?: boolean;
|
||||
isEdited?: boolean;
|
||||
@@ -48,8 +48,10 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if subtitle}
|
||||
{#if typeof subtitle === 'string'}
|
||||
<p id={subtitleId} class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
|
||||
{:else}
|
||||
{@render subtitle?.()}
|
||||
{/if}
|
||||
{@render children?.()}
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="mb-4 w-full">
|
||||
<div class="w-full">
|
||||
<div class="flex h-6.5 place-items-center gap-1">
|
||||
<label class="font-medium text-primary text-sm" for={label}>{label}</label>
|
||||
{#if required}
|
||||
|
||||
62
web/src/lib/modals/SystemSettingsModal.svelte
Normal file
62
web/src/lib/modals/SystemSettingsModal.svelte
Normal file
@@ -0,0 +1,62 @@
|
||||
<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, resolveSetting } from '$lib/services/system-config.service';
|
||||
import type { SystemConfigContext } from '$lib/types';
|
||||
import type { SystemConfigDto } from '@immich/sdk';
|
||||
import { Button, FormModal, type ModalSize } from '@immich/ui';
|
||||
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 = 'large', onBeforeSave, child }: Props = $props();
|
||||
|
||||
const disabled = $derived(featureFlagsManager.value.configFile);
|
||||
const config = $derived(systemConfigManager.value);
|
||||
let configToEdit = $state(systemConfigManager.cloneValue());
|
||||
const { settings } = $derived(getSystemConfigActions($t, featureFlagsManager.value, systemConfigManager.value));
|
||||
const setting = $derived(resolveSetting(settings, 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} 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}
|
||||
@@ -28,7 +28,7 @@ export const getLibrariesActions = ($t: MessageFormatter, libraries: LibraryResp
|
||||
title: $t('scan_all_libraries'),
|
||||
type: $t('command'),
|
||||
icon: mdiSync,
|
||||
onAction: () => void handleScanAllLibraries(),
|
||||
onAction: () => handleScanAllLibraries(),
|
||||
shortcuts: { shift: true, key: 'r' },
|
||||
$if: () => libraries.length > 0,
|
||||
};
|
||||
@@ -37,7 +37,7 @@ export const getLibrariesActions = ($t: MessageFormatter, libraries: LibraryResp
|
||||
title: $t('create_library'),
|
||||
type: $t('command'),
|
||||
icon: mdiPlusBoxOutline,
|
||||
onAction: () => void handleCreateLibrary(),
|
||||
onAction: () => handleCreateLibrary(),
|
||||
shortcuts: { shift: true, key: 'n' },
|
||||
};
|
||||
|
||||
@@ -49,7 +49,7 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
|
||||
icon: mdiPencilOutline,
|
||||
type: $t('command'),
|
||||
title: $t('rename'),
|
||||
onAction: () => void modalManager.show(LibraryRenameModal, { library }),
|
||||
onAction: () => modalManager.show(LibraryRenameModal, { library }),
|
||||
shortcuts: { key: 'r' },
|
||||
};
|
||||
|
||||
@@ -58,7 +58,7 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
|
||||
type: $t('command'),
|
||||
title: $t('delete'),
|
||||
color: 'danger',
|
||||
onAction: () => void handleDeleteLibrary(library),
|
||||
onAction: () => handleDeleteLibrary(library),
|
||||
shortcuts: { key: 'Backspace' },
|
||||
};
|
||||
|
||||
@@ -66,21 +66,21 @@ export const getLibraryActions = ($t: MessageFormatter, library: LibraryResponse
|
||||
icon: mdiPlusBoxOutline,
|
||||
type: $t('command'),
|
||||
title: $t('add'),
|
||||
onAction: () => void modalManager.show(LibraryFolderAddModal, { library }),
|
||||
onAction: () => modalManager.show(LibraryFolderAddModal, { library }),
|
||||
};
|
||||
|
||||
const AddExclusionPattern: ActionItem = {
|
||||
icon: mdiPlusBoxOutline,
|
||||
type: $t('command'),
|
||||
title: $t('add'),
|
||||
onAction: () => void modalManager.show(LibraryExclusionPatternAddModal, { library }),
|
||||
onAction: () => modalManager.show(LibraryExclusionPatternAddModal, { library }),
|
||||
};
|
||||
|
||||
const Scan: ActionItem = {
|
||||
icon: mdiSync,
|
||||
type: $t('command'),
|
||||
title: $t('scan_library'),
|
||||
onAction: () => void handleScanLibrary(library),
|
||||
onAction: () => handleScanLibrary(library),
|
||||
shortcuts: { shift: true, key: 'r' },
|
||||
};
|
||||
|
||||
@@ -92,14 +92,14 @@ export const getLibraryFolderActions = ($t: MessageFormatter, library: LibraryRe
|
||||
icon: mdiPencilOutline,
|
||||
type: $t('command'),
|
||||
title: $t('edit'),
|
||||
onAction: () => void modalManager.show(LibraryFolderEditModal, { folder, library }),
|
||||
onAction: () => modalManager.show(LibraryFolderEditModal, { folder, library }),
|
||||
};
|
||||
|
||||
const Delete: ActionItem = {
|
||||
icon: mdiTrashCanOutline,
|
||||
type: $t('command'),
|
||||
title: $t('delete'),
|
||||
onAction: () => void handleDeleteLibraryFolder(library, folder),
|
||||
onAction: () => handleDeleteLibraryFolder(library, folder),
|
||||
};
|
||||
|
||||
return { Edit, Delete };
|
||||
@@ -114,14 +114,14 @@ export const getLibraryExclusionPatternActions = (
|
||||
icon: mdiPencilOutline,
|
||||
type: $t('command'),
|
||||
title: $t('edit'),
|
||||
onAction: () => void modalManager.show(LibraryExclusionPatternEditModal, { exclusionPattern, library }),
|
||||
onAction: () => modalManager.show(LibraryExclusionPatternEditModal, { exclusionPattern, library }),
|
||||
};
|
||||
|
||||
const Delete: ActionItem = {
|
||||
icon: mdiTrashCanOutline,
|
||||
type: $t('command'),
|
||||
title: $t('delete'),
|
||||
onAction: () => void handleDeleteExclusionPattern(library, exclusionPattern),
|
||||
onAction: () => handleDeleteExclusionPattern(library, exclusionPattern),
|
||||
};
|
||||
|
||||
return { Edit, Delete };
|
||||
@@ -273,7 +273,7 @@ const handleDeleteLibraryFolder = async (library: LibraryResponseDto, folder: st
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -285,10 +285,7 @@ const handleDeleteLibraryFolder = async (library: LibraryResponseDto, folder: st
|
||||
toastManager.success($t('admin.library_updated'));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_update_library'));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const handleAddLibraryExclusionPattern = async (library: LibraryResponseDto, exclusionPattern: string) => {
|
||||
@@ -345,9 +342,8 @@ const handleDeleteExclusionPattern = async (library: LibraryResponseDto, exclusi
|
||||
const $t = await getFormatter();
|
||||
|
||||
const confirmed = await modalManager.showDialog({ prompt: $t('admin.library_remove_exclusion_pattern_prompt') });
|
||||
|
||||
if (!confirmed) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -361,8 +357,5 @@ const handleDeleteExclusionPattern = async (library: LibraryResponseDto, exclusi
|
||||
toastManager.success($t('admin.library_updated'));
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_update_library'));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { queueManager } from '$lib/managers/queue-manager.svelte';
|
||||
import JobCreateModal from '$lib/modals/JobCreateModal.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import type { HeaderButtonActionItem } from '$lib/types';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { emptyQueue, getQueue, QueueName, updateQueue, type QueueResponseDto } from '@immich/sdk';
|
||||
import {
|
||||
emptyQueue,
|
||||
getQueue,
|
||||
QueueCommand,
|
||||
QueueName,
|
||||
runQueueCommandLegacy,
|
||||
updateQueue,
|
||||
type QueueResponseDto,
|
||||
} from '@immich/sdk';
|
||||
import { modalManager, toastManager, type ActionItem, type IconLike } from '@immich/ui';
|
||||
import {
|
||||
mdiClose,
|
||||
@@ -23,7 +32,6 @@ import {
|
||||
mdiPlay,
|
||||
mdiPlus,
|
||||
mdiStateMachine,
|
||||
mdiSync,
|
||||
mdiTable,
|
||||
mdiTagFaces,
|
||||
mdiTrashCanOutline,
|
||||
@@ -31,7 +39,6 @@ import {
|
||||
mdiVideo,
|
||||
} from '@mdi/js';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
type QueueItem = {
|
||||
icon: IconLike;
|
||||
@@ -39,15 +46,17 @@ type QueueItem = {
|
||||
subtitle?: string;
|
||||
};
|
||||
|
||||
export const getQueuesActions = ($t: MessageFormatter) => {
|
||||
const ViewQueues: ActionItem = {
|
||||
title: $t('admin.queues'),
|
||||
description: $t('admin.queues_page_description'),
|
||||
icon: mdiSync,
|
||||
type: $t('page'),
|
||||
isGlobal: true,
|
||||
$if: () => get(user)?.isAdmin,
|
||||
onAction: () => goto(AppRoute.ADMIN_QUEUES),
|
||||
export const getQueuesActions = ($t: MessageFormatter, queues: QueueResponseDto[] | undefined) => {
|
||||
const pausedQueues = (queues ?? []).filter(({ isPaused }) => isPaused).map(({ name }) => name);
|
||||
|
||||
const ResumePaused: HeaderButtonActionItem = {
|
||||
title: $t('resume_paused_jobs', { values: { count: pausedQueues.length } }),
|
||||
$if: () => pausedQueues.length > 0,
|
||||
icon: mdiPlay,
|
||||
onAction: () => handleResumePausedJobs(pausedQueues),
|
||||
data: {
|
||||
title: pausedQueues.join(', '),
|
||||
},
|
||||
};
|
||||
|
||||
const CreateJob: ActionItem = {
|
||||
@@ -68,7 +77,7 @@ export const getQueuesActions = ($t: MessageFormatter) => {
|
||||
onAction: () => goto(`${AppRoute.ADMIN_SETTINGS}?isOpen=job`),
|
||||
};
|
||||
|
||||
return { ViewQueues, ManageConcurrency, CreateJob };
|
||||
return { ResumePaused, ManageConcurrency, CreateJob };
|
||||
};
|
||||
|
||||
export const getQueueActions = ($t: MessageFormatter, queue: QueueResponseDto) => {
|
||||
@@ -126,6 +135,19 @@ export const handleEmptyQueue = async (queue: QueueResponseDto) => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleResumePausedJobs = async (queues: QueueName[]) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
try {
|
||||
for (const name of queues) {
|
||||
await runQueueCommandLegacy({ name, queueCommandDto: { command: QueueCommand.Resume, force: false } });
|
||||
}
|
||||
await queueManager.refresh();
|
||||
} catch (error) {
|
||||
handleError(error, $t('admin.failed_job_command', { values: { command: 'resume', job: 'paused jobs' } }));
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemoveFailedJobs = async (queue: QueueResponseDto) => {
|
||||
const $t = await getFormatter();
|
||||
|
||||
|
||||
@@ -24,26 +24,26 @@ export const getSharedLinkActions = ($t: MessageFormatter, sharedLink: SharedLin
|
||||
const Edit: ActionItem = {
|
||||
title: $t('edit_link'),
|
||||
icon: mdiPencilOutline,
|
||||
onAction: () => void goto(`${AppRoute.SHARED_LINKS}/${sharedLink.id}`),
|
||||
onAction: () => goto(`${AppRoute.SHARED_LINKS}/${sharedLink.id}`),
|
||||
};
|
||||
|
||||
const Delete: ActionItem = {
|
||||
title: $t('delete_link'),
|
||||
icon: mdiTrashCanOutline,
|
||||
color: 'danger',
|
||||
onAction: () => void handleDeleteSharedLink(sharedLink),
|
||||
onAction: () => handleDeleteSharedLink(sharedLink),
|
||||
};
|
||||
|
||||
const Copy: ActionItem = {
|
||||
title: $t('copy_link'),
|
||||
icon: mdiContentCopy,
|
||||
onAction: () => void copyToClipboard(asUrl(sharedLink)),
|
||||
onAction: () => copyToClipboard(asUrl(sharedLink)),
|
||||
};
|
||||
|
||||
const ViewQrCode: ActionItem = {
|
||||
title: $t('view_qr_code'),
|
||||
icon: mdiQrcode,
|
||||
onAction: () => void handleShowSharedLinkQrCode(sharedLink),
|
||||
onAction: () => handleShowSharedLinkQrCode(sharedLink),
|
||||
};
|
||||
|
||||
return { Edit, Delete, Copy, ViewQrCode };
|
||||
@@ -88,7 +88,7 @@ export const handleUpdateSharedLink = async (sharedLink: SharedLinkResponseDto,
|
||||
}
|
||||
};
|
||||
|
||||
export const handleDeleteSharedLink = async (sharedLink: SharedLinkResponseDto): Promise<boolean> => {
|
||||
const handleDeleteSharedLink = async (sharedLink: SharedLinkResponseDto) => {
|
||||
const $t = await getFormatter();
|
||||
const success = await modalManager.showDialog({
|
||||
title: $t('delete_shared_link'),
|
||||
@@ -96,17 +96,15 @@ export const handleDeleteSharedLink = async (sharedLink: SharedLinkResponseDto):
|
||||
confirmText: $t('delete'),
|
||||
});
|
||||
if (!success) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await removeSharedLink({ id: sharedLink.id });
|
||||
eventManager.emit('SharedLinkDelete', sharedLink);
|
||||
toastManager.success($t('deleted_shared_link'));
|
||||
return true;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_delete_shared_link'));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,26 +1,321 @@
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { downloadManager } from '$lib/managers/download-manager.svelte';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import AuthDisableLoginConfirmModal from '$lib/modals/AuthDisableLoginConfirmModal.svelte';
|
||||
import type { SystemConfigContext } from '$lib/types';
|
||||
import { copyToClipboard } from '$lib/utils';
|
||||
import { downloadBlob } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { getConfig, updateConfig, type ServerFeaturesDto, type SystemConfigDto } from '@immich/sdk';
|
||||
import { toastManager, type ActionItem } from '@immich/ui';
|
||||
import { mdiContentCopy, mdiDownload, mdiUpload } from '@mdi/js';
|
||||
import { getConfig, unlinkAllOAuthAccountsAdmin, updateConfig, type ServerFeaturesDto, type SystemConfigDto } from '@immich/sdk';
|
||||
import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
||||
import {
|
||||
mdiAccountOutline,
|
||||
mdiBackupRestore,
|
||||
mdiBellOutline,
|
||||
mdiBookshelf,
|
||||
mdiClockOutline,
|
||||
mdiContentCopy,
|
||||
mdiDatabaseOutline,
|
||||
mdiDownload,
|
||||
mdiFileDocumentOutline,
|
||||
mdiFolderOutline,
|
||||
mdiLockOutline,
|
||||
mdiMapMarkerOutline,
|
||||
mdiPaletteOutline,
|
||||
mdiRestart,
|
||||
mdiRobotOutline,
|
||||
mdiServerOutline,
|
||||
mdiSync,
|
||||
mdiTrashCanOutline,
|
||||
mdiUpdate,
|
||||
mdiUpload,
|
||||
mdiVideoOutline
|
||||
} from '@mdi/js';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
|
||||
type SettingsGroup = {
|
||||
title: string,
|
||||
subtitle?: string;
|
||||
items: SettingItem[];
|
||||
}
|
||||
|
||||
type SettingItem = {
|
||||
title: string; subtitle: string; href: string; icon: string;
|
||||
};
|
||||
|
||||
export const resolveSetting = (groups: SettingsGroup[], pathname: string) => {
|
||||
for (const group of groups) {
|
||||
for (const item of group.items) {
|
||||
if (item.href === pathname) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const getSystemConfigActions = (
|
||||
$t: MessageFormatter,
|
||||
featureFlags: ServerFeaturesDto,
|
||||
config: SystemConfigDto,
|
||||
) => {
|
||||
const settings: SettingsGroup[] = [
|
||||
{
|
||||
|
||||
title: $t('admin.authentication_settings'),
|
||||
subtitle: $t('admin.authentication_settings_description'),
|
||||
|
||||
items: [
|
||||
{
|
||||
title:$t('admin.password_settings'), subtitle: $t('admin.password_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/password`,
|
||||
icon: mdiLockOutline,
|
||||
},
|
||||
{
|
||||
|
||||
title:$t('admin.oauth_settings'), subtitle:$t('admin.oauth_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/oauth`,
|
||||
icon: mdiFileDocumentOutline,
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'General', items: [
|
||||
// {
|
||||
// title: $t('admin.image_settings'),
|
||||
// subtitle: $t('admin.image_settings_description'),
|
||||
// href: `${AppRoute.ADMIN_SETTINGS}/image`,
|
||||
// icon: mdiImageOutline,
|
||||
// },
|
||||
{
|
||||
title: $t('admin.library_settings'),
|
||||
subtitle: $t('admin.library_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/library`,
|
||||
icon: mdiBookshelf,
|
||||
},
|
||||
// {
|
||||
// title: $t('admin.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.notification_settings'),
|
||||
subtitle: $t('admin.notification_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/notifications`,
|
||||
icon: mdiBellOutline,
|
||||
},
|
||||
{
|
||||
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: 'Image', items: [
|
||||
{
|
||||
title: 'General settings',
|
||||
subtitle: $t('admin.transcoding_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
|
||||
icon: mdiVideoOutline,
|
||||
},
|
||||
{
|
||||
title: 'Thumbnail settings',
|
||||
subtitle: $t('admin.transcoding_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
|
||||
icon: mdiVideoOutline,
|
||||
},
|
||||
{
|
||||
title: 'Preview settings',
|
||||
subtitle: $t('admin.transcoding_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
|
||||
icon: mdiVideoOutline,
|
||||
},
|
||||
{
|
||||
title: 'Full-size settings',
|
||||
subtitle: $t('admin.transcoding_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
|
||||
icon: mdiVideoOutline,
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Video', items: [
|
||||
{
|
||||
title: 'General settings',
|
||||
subtitle: $t('admin.transcoding_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
|
||||
icon: mdiVideoOutline,
|
||||
},
|
||||
{
|
||||
title: 'Transcoding Policies',
|
||||
subtitle: $t('admin.transcoding_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
|
||||
icon: mdiVideoOutline,
|
||||
},
|
||||
{
|
||||
title: 'Hardware Acceleration',
|
||||
subtitle: $t('admin.transcoding_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
|
||||
icon: mdiVideoOutline,
|
||||
},
|
||||
{
|
||||
title: 'Encoding Options',
|
||||
subtitle: $t('admin.transcoding_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
|
||||
icon: mdiVideoOutline,
|
||||
},
|
||||
{
|
||||
title: $t('admin.transcoding_settings'),
|
||||
subtitle: $t('admin.transcoding_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
|
||||
icon: mdiVideoOutline,
|
||||
},
|
||||
{
|
||||
title: 'Advanced Settings',
|
||||
subtitle: $t('admin.transcoding_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/video-transcoding`,
|
||||
icon: mdiVideoOutline,
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Machine learning', items: [
|
||||
{
|
||||
title: 'Connection settings',
|
||||
subtitle: $t('admin.machine_learning_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/machine-learning`,
|
||||
icon: mdiRobotOutline,
|
||||
},
|
||||
// {
|
||||
// title: $t('admin.machine_learning_settings'),
|
||||
// subtitle: $t('admin.machine_learning_settings_description'),
|
||||
// href: `${AppRoute.ADMIN_SETTINGS}/machine-learning`,
|
||||
// icon: mdiRobotOutline,
|
||||
// },
|
||||
{
|
||||
title: 'Search',
|
||||
subtitle: $t('admin.machine_learning_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/machine-learning`,
|
||||
icon: mdiRobotOutline,
|
||||
},
|
||||
{
|
||||
title: 'Duplicate Detection',
|
||||
subtitle: $t('admin.machine_learning_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/machine-learning`,
|
||||
icon: mdiRobotOutline,
|
||||
},
|
||||
{
|
||||
title: 'Facial Recognition',
|
||||
subtitle: $t('admin.machine_learning_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/machine-learning`,
|
||||
icon: mdiRobotOutline,
|
||||
},
|
||||
{
|
||||
title: 'OCR',
|
||||
subtitle: $t('admin.machine_learning_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/machine-learning`,
|
||||
icon: mdiRobotOutline,
|
||||
},
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
title: 'Job settings', items: [
|
||||
{
|
||||
title: 'General settings',
|
||||
subtitle: $t('admin.server_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/server`,
|
||||
icon: mdiServerOutline,
|
||||
},
|
||||
{
|
||||
title: $t('admin.nightly_tasks_settings'),
|
||||
subtitle: $t('admin.nightly_tasks_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/nightly-tasks`,
|
||||
icon: mdiClockOutline,
|
||||
},
|
||||
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Server settings', items: [
|
||||
{
|
||||
title: 'General settings',
|
||||
subtitle: $t('admin.server_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/server`,
|
||||
icon: mdiServerOutline,
|
||||
},
|
||||
{
|
||||
title: $t('admin.authentication_settings'),
|
||||
subtitle: $t('admin.authentication_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/authentication`,
|
||||
icon: mdiLockOutline,
|
||||
},
|
||||
|
||||
{
|
||||
title: $t('admin.job_settings'),
|
||||
subtitle: $t('admin.job_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/job`,
|
||||
icon: mdiSync,
|
||||
},
|
||||
{
|
||||
title: $t('admin.backup_settings'),
|
||||
subtitle: $t('admin.backup_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/backup`,
|
||||
icon: mdiBackupRestore,
|
||||
},
|
||||
{
|
||||
title: $t('admin.version_check_settings'),
|
||||
subtitle: $t('admin.version_check_settings_description'),
|
||||
href: `${AppRoute.ADMIN_SETTINGS}/version-check`,
|
||||
icon: mdiUpdate,
|
||||
},
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
const CopyToClipboard: ActionItem = {
|
||||
title: $t('copy_to_clipboard'),
|
||||
description: $t('admin.copy_config_to_clipboard_description'),
|
||||
type: $t('command'),
|
||||
icon: mdiContentCopy,
|
||||
onAction: () => void handleCopyToClipboard(config),
|
||||
onAction: () => handleCopyToClipboard(config),
|
||||
shortcuts: { shift: true, key: 'c' },
|
||||
};
|
||||
|
||||
@@ -46,7 +341,7 @@ export const getSystemConfigActions = (
|
||||
shortcuts: { shift: true, key: 'u' },
|
||||
};
|
||||
|
||||
return { CopyToClipboard, Download, Upload };
|
||||
return { settings, CopyToClipboard, Download, Upload };
|
||||
};
|
||||
|
||||
export const handleSystemConfigSave = async (update: Partial<SystemConfigDto>) => {
|
||||
@@ -72,12 +367,12 @@ export const handleSystemConfigSave = async (update: Partial<SystemConfigDto>) =
|
||||
const jsonReplacer = (_key: string, value: unknown) =>
|
||||
value instanceof Object && !Array.isArray(value)
|
||||
? Object.keys(value)
|
||||
.sort()
|
||||
// eslint-disable-next-line unicorn/no-array-reduce
|
||||
.reduce((sorted: { [key: string]: unknown }, key) => {
|
||||
sorted[key] = (value as { [key: string]: unknown })[key];
|
||||
return sorted;
|
||||
}, {})
|
||||
.sort()
|
||||
// eslint-disable-next-line unicorn/no-array-reduce
|
||||
.reduce((sorted: { [key: string]: unknown }, key) => {
|
||||
sorted[key] = (value as { [key: string]: unknown })[key];
|
||||
return sorted;
|
||||
}, {})
|
||||
: value;
|
||||
|
||||
export const handleCopyToClipboard = async (config: SystemConfigDto) => {
|
||||
@@ -114,3 +409,38 @@ export const handleUploadConfig = () => {
|
||||
});
|
||||
input.remove();
|
||||
};
|
||||
|
||||
|
||||
export const handleUnlinkAllOAuthAccounts = async () => {
|
||||
const $t = await getFormatter();
|
||||
const confirmed = await modalManager.showDialog({
|
||||
icon: mdiRestart,
|
||||
title: $t('admin.unlink_all_oauth_accounts'),
|
||||
prompt: $t('admin.unlink_all_oauth_accounts_prompt'),
|
||||
confirmColor: 'danger',
|
||||
});
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await unlinkAllOAuthAccountsAdmin();
|
||||
toastManager.success();
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.something_went_wrong'));
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
export const onBeforeSave = async ({ configToEdit }: SystemConfigContext) => {
|
||||
const allMethodsDisabled = !configToEdit.oauth.enabled && !configToEdit.passwordLogin.enabled;
|
||||
if (allMethodsDisabled) {
|
||||
const isConfirmed = await modalManager.show(AuthDisableLoginConfirmModal, {});
|
||||
if (!isConfirmed) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { goto } from '$app/navigation';
|
||||
import { eventManager } from '$lib/managers/event-manager.svelte';
|
||||
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
||||
import PasswordResetSuccessModal from '$lib/modals/PasswordResetSuccessModal.svelte';
|
||||
import UserCreateModal from '$lib/modals/UserCreateModal.svelte';
|
||||
import UserDeleteConfirmModal from '$lib/modals/UserDeleteConfirmModal.svelte';
|
||||
import UserEditModal from '$lib/modals/UserEditModal.svelte';
|
||||
import UserRestoreConfirmModal from '$lib/modals/UserRestoreConfirmModal.svelte';
|
||||
import { user as authUser } from '$lib/stores/user.store';
|
||||
import type { HeaderButtonActionItem } from '$lib/types';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import {
|
||||
@@ -28,6 +30,7 @@ import {
|
||||
mdiPlusBoxOutline,
|
||||
mdiTrashCanOutline,
|
||||
} from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
import type { MessageFormatter } from 'svelte-i18n';
|
||||
import { get } from 'svelte/store';
|
||||
|
||||
@@ -36,7 +39,7 @@ export const getUserAdminsActions = ($t: MessageFormatter) => {
|
||||
title: $t('create_user'),
|
||||
type: $t('command'),
|
||||
icon: mdiPlusBoxOutline,
|
||||
onAction: () => void modalManager.show(UserCreateModal, {}),
|
||||
onAction: () => modalManager.show(UserCreateModal, {}),
|
||||
shortcuts: { shift: true, key: 'n' },
|
||||
};
|
||||
|
||||
@@ -60,11 +63,17 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
|
||||
shortcuts: { key: 'Backspace' },
|
||||
};
|
||||
|
||||
const Restore: ActionItem = {
|
||||
const getDeleteDate = (deletedAt: string): Date =>
|
||||
DateTime.fromISO(deletedAt).plus({ days: serverConfigManager.value.userDeleteDelay }).toJSDate();
|
||||
|
||||
const Restore: HeaderButtonActionItem = {
|
||||
icon: mdiDeleteRestore,
|
||||
title: $t('restore'),
|
||||
type: $t('command'),
|
||||
color: 'primary',
|
||||
data: {
|
||||
title: $t('admin.user_restore_scheduled_removal', { values: { date: getDeleteDate(user.deletedAt!) } }),
|
||||
},
|
||||
$if: () => !!user.deletedAt && user.status === UserStatus.Deleted,
|
||||
onAction: () => modalManager.show(UserRestoreConfirmModal, { user }),
|
||||
};
|
||||
@@ -74,14 +83,14 @@ export const getUserAdminActions = ($t: MessageFormatter, user: UserAdminRespons
|
||||
title: $t('reset_password'),
|
||||
type: $t('command'),
|
||||
$if: () => get(authUser).id !== user.id,
|
||||
onAction: () => void handleResetPasswordUserAdmin(user),
|
||||
onAction: () => handleResetPasswordUserAdmin(user),
|
||||
};
|
||||
|
||||
const ResetPinCode: ActionItem = {
|
||||
icon: mdiLockSmart,
|
||||
type: $t('command'),
|
||||
title: $t('reset_pin_code'),
|
||||
onAction: () => void handleResetPinCodeUserAdmin(user),
|
||||
onAction: () => handleResetPinCodeUserAdmin(user),
|
||||
};
|
||||
|
||||
return { Update, Delete, Restore, ResetPassword, ResetPinCode };
|
||||
@@ -162,12 +171,12 @@ const generatePassword = (length: number = 16) => {
|
||||
return generatedPassword;
|
||||
};
|
||||
|
||||
export const handleResetPasswordUserAdmin = async (user: UserAdminResponseDto) => {
|
||||
const handleResetPasswordUserAdmin = async (user: UserAdminResponseDto) => {
|
||||
const $t = await getFormatter();
|
||||
const prompt = $t('admin.confirm_user_password_reset', { values: { user: user.name } });
|
||||
const success = await modalManager.showDialog({ prompt });
|
||||
if (!success) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -176,28 +185,24 @@ export const handleResetPasswordUserAdmin = async (user: UserAdminResponseDto) =
|
||||
eventManager.emit('UserAdminUpdate', response);
|
||||
toastManager.success();
|
||||
await modalManager.show(PasswordResetSuccessModal, { newPassword: dto.password });
|
||||
return true;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_reset_password'));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export const handleResetPinCodeUserAdmin = async (user: UserAdminResponseDto) => {
|
||||
const handleResetPinCodeUserAdmin = async (user: UserAdminResponseDto) => {
|
||||
const $t = await getFormatter();
|
||||
const prompt = $t('admin.confirm_user_pin_code_reset', { values: { user: user.name } });
|
||||
const success = await modalManager.showDialog({ prompt });
|
||||
if (!success) {
|
||||
return false;
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await updateUserAdmin({ id: user.id, userAdminUpdateDto: { pinCode: null } });
|
||||
eventManager.emit('UserAdminUpdate', response);
|
||||
toastManager.success($t('pin_code_reset_successfully'));
|
||||
return true;
|
||||
} catch (error) {
|
||||
handleError(error, $t('errors.unable_to_reset_pin_code'));
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { QueueResponseDto, ServerVersionResponseDto } from '@immich/sdk';
|
||||
import type { QueueResponseDto, ServerVersionResponseDto, SystemConfigDto } from '@immich/sdk';
|
||||
import type { ActionItem } from '@immich/ui';
|
||||
|
||||
export interface ReleaseEvent {
|
||||
isAvailable: boolean;
|
||||
@@ -9,3 +10,7 @@ export interface ReleaseEvent {
|
||||
}
|
||||
|
||||
export type QueueSnapshot = { timestamp: number; snapshot?: QueueResponseDto[] };
|
||||
|
||||
export type HeaderButtonActionItem = ActionItem & { data?: { title?: string } };
|
||||
|
||||
export type SystemConfigContext = { disabled: boolean; config: SystemConfigDto; configToEdit: SystemConfigDto };
|
||||
|
||||
@@ -14,15 +14,15 @@
|
||||
import { themeManager } from '$lib/managers/theme-manager.svelte';
|
||||
import ServerRestartingModal from '$lib/modals/ServerRestartingModal.svelte';
|
||||
import VersionAnnouncementModal from '$lib/modals/VersionAnnouncementModal.svelte';
|
||||
import { getQueuesActions } from '$lib/services/queue.service';
|
||||
import { sidebarStore } from '$lib/stores/sidebar.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import { closeWebsocketConnection, openWebsocketConnection, websocketStore } from '$lib/stores/websocket';
|
||||
import type { ReleaseEvent } from '$lib/types';
|
||||
import { copyToClipboard, getReleaseType, semverToName } from '$lib/utils';
|
||||
import { maintenanceShouldRedirect } from '$lib/utils/maintenance';
|
||||
import { isAssetViewerRoute } from '$lib/utils/navigation';
|
||||
import { CommandPaletteContext, modalManager, setTranslations, type ActionItem } from '@immich/ui';
|
||||
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiThemeLightDark } from '@mdi/js';
|
||||
import { CommandPaletteContext, modalManager, setTranslations, toastManager, type ActionItem } from '@immich/ui';
|
||||
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiThemeLightDark } from '@mdi/js';
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import '../app.css';
|
||||
@@ -38,6 +38,7 @@
|
||||
hide_password: $t('hide_password'),
|
||||
confirm: $t('confirm'),
|
||||
cancel: $t('cancel'),
|
||||
save: $t('save'),
|
||||
toast_success_title: $t('success'),
|
||||
toast_info_title: $t('info'),
|
||||
toast_warning_title: $t('warning'),
|
||||
@@ -53,6 +54,8 @@
|
||||
return new URL(page.url.pathname + page.url.search, 'https://my.immich.app');
|
||||
};
|
||||
|
||||
toastManager.setOptions({ class: 'top-16' });
|
||||
|
||||
onMount(() => {
|
||||
const element = document.querySelector('#stencil');
|
||||
element?.remove();
|
||||
@@ -62,6 +65,10 @@
|
||||
eventManager.emit('AppInit');
|
||||
|
||||
beforeNavigate(({ from, to }) => {
|
||||
if (sidebarStore.isOpen) {
|
||||
sidebarStore.reset();
|
||||
}
|
||||
|
||||
if (isAssetViewerRoute(from) && isAssetViewerRoute(to)) {
|
||||
return;
|
||||
}
|
||||
@@ -149,6 +156,13 @@
|
||||
icon: mdiCog,
|
||||
onAction: () => goto(AppRoute.ADMIN_SETTINGS),
|
||||
},
|
||||
{
|
||||
title: $t('admin.queues'),
|
||||
description: $t('admin.queues_page_description'),
|
||||
icon: mdiSync,
|
||||
type: $t('page'),
|
||||
onAction: () => goto(AppRoute.ADMIN_QUEUES),
|
||||
},
|
||||
{
|
||||
title: $t('external_libraries'),
|
||||
description: $t('admin.external_libraries_page_description'),
|
||||
@@ -163,7 +177,7 @@
|
||||
},
|
||||
].map((route) => ({ ...route, type: $t('page'), isGlobal: true, $if: () => $user?.isAdmin }));
|
||||
|
||||
const commands = $derived([...userCommands, ...adminCommands, ...Object.values(getQueuesActions($t))]);
|
||||
const commands = $derived([...userCommands, ...adminCommands]);
|
||||
</script>
|
||||
|
||||
<OnEvents {onReleaseEvent} />
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import HeaderButton from '$lib/components/HeaderButton.svelte';
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
@@ -60,17 +59,11 @@
|
||||
|
||||
<CommandPaletteContext commands={[Create, ScanAll]} />
|
||||
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
||||
{#snippet buttons()}
|
||||
<div class="flex justify-end gap-2">
|
||||
<HeaderButton action={ScanAll} />
|
||||
<HeaderButton action={Create} />
|
||||
</div>
|
||||
{/snippet}
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]} actions={[ScanAll, Create]}>
|
||||
<section class="my-4">
|
||||
<div class="flex flex-col items-center gap-2" in:fade={{ duration: 500 }}>
|
||||
{#if libraries.length > 0}
|
||||
<table class="w-3/4 text-start">
|
||||
<table class="text-start">
|
||||
<thead
|
||||
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray"
|
||||
>
|
||||
|
||||
@@ -23,7 +23,7 @@ export const load = (async ({ url }) => {
|
||||
statistics: Object.fromEntries(statistics),
|
||||
owners: Object.fromEntries(owners),
|
||||
meta: {
|
||||
title: $t('admin.external_library_management'),
|
||||
title: $t('external_libraries'),
|
||||
},
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import emptyFoldersUrl from '$lib/assets/empty-folders.svg';
|
||||
import HeaderButton from '$lib/components/HeaderButton.svelte';
|
||||
import HeaderActionButton from '$lib/components/HeaderActionButton.svelte';
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import ServerStatisticsCard from '$lib/components/server-statistics/ServerStatisticsCard.svelte';
|
||||
@@ -53,18 +53,9 @@
|
||||
<CommandPaletteContext commands={[Rename, Delete, AddFolder, AddExclusionPattern, Scan]} />
|
||||
|
||||
<AdminPageLayout
|
||||
breadcrumbs={[
|
||||
{ title: $t('admin.external_library_management'), href: AppRoute.ADMIN_LIBRARY_MANAGEMENT },
|
||||
{ title: library.name },
|
||||
]}
|
||||
breadcrumbs={[{ title: $t('external_libraries'), href: AppRoute.ADMIN_LIBRARY_MANAGEMENT }, { title: library.name }]}
|
||||
actions={[Scan, Rename, Delete]}
|
||||
>
|
||||
{#snippet buttons()}
|
||||
<div class="flex justify-end gap-2">
|
||||
<HeaderButton action={Scan} />
|
||||
<HeaderButton action={Rename} />
|
||||
<HeaderButton action={Delete} />
|
||||
</div>
|
||||
{/snippet}
|
||||
<Container size="large" center>
|
||||
<div class="grid gap-4 grid-cols-1 lg:grid-cols-2 w-full">
|
||||
<Heading tag="h1" size="large" class="col-span-full my-4">{library.name}</Heading>
|
||||
@@ -80,7 +71,7 @@
|
||||
<Icon icon={mdiFolderOutline} size="1.5rem" />
|
||||
<CardTitle>{$t('folders')}</CardTitle>
|
||||
</div>
|
||||
<HeaderButton action={AddFolder} />
|
||||
<HeaderActionButton action={AddFolder} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
@@ -120,7 +111,7 @@
|
||||
<Icon icon={mdiFilterMinusOutline} size="1.5rem" />
|
||||
<CardTitle>{$t('exclusion_pattern')}</CardTitle>
|
||||
</div>
|
||||
<HeaderButton action={AddExclusionPattern} />
|
||||
<HeaderActionButton action={AddExclusionPattern} />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
<script lang="ts">
|
||||
import HeaderButton from '$lib/components/HeaderButton.svelte';
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import JobsPanel from '$lib/components/QueuePanel.svelte';
|
||||
import { queueManager } from '$lib/managers/queue-manager.svelte';
|
||||
import { getQueuesActions } from '$lib/services/queue.service';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { QueueCommand, runQueueCommandLegacy, type QueueResponseDto } from '@immich/sdk';
|
||||
import { Button, CommandPaletteContext, HStack, Text, type ActionItem } from '@immich/ui';
|
||||
import { mdiPlay } from '@mdi/js';
|
||||
import { type QueueResponseDto } from '@immich/sdk';
|
||||
import { CommandPaletteContext, type ActionItem } from '@immich/ui';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
@@ -22,20 +19,8 @@
|
||||
onMount(() => queueManager.listen());
|
||||
|
||||
let queues = $derived<QueueResponseDto[]>(queueManager.queues);
|
||||
const pausedQueues = $derived(queues.filter(({ isPaused }) => isPaused).map(({ name }) => name));
|
||||
|
||||
const handleResumePausedJobs = async () => {
|
||||
try {
|
||||
for (const name of pausedQueues) {
|
||||
await runQueueCommandLegacy({ name, queueCommandDto: { command: QueueCommand.Resume, force: false } });
|
||||
}
|
||||
await queueManager.refresh();
|
||||
} catch (error) {
|
||||
handleError(error, $t('admin.failed_job_command', { values: { command: 'resume', job: 'paused jobs' } }));
|
||||
}
|
||||
};
|
||||
|
||||
const { CreateJob, ManageConcurrency } = $derived(getQueuesActions($t));
|
||||
const { ResumePaused, CreateJob, ManageConcurrency } = $derived(getQueuesActions($t, queueManager.queues));
|
||||
const commands: ActionItem[] = $derived([CreateJob, ManageConcurrency]);
|
||||
|
||||
const onQueueUpdate = (update: QueueResponseDto) => {
|
||||
@@ -52,27 +37,7 @@
|
||||
|
||||
<OnEvents {onQueueUpdate} />
|
||||
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
||||
{#snippet buttons()}
|
||||
<HStack gap={0}>
|
||||
{#if pausedQueues.length > 0}
|
||||
<Button
|
||||
leadingIcon={mdiPlay}
|
||||
onclick={handleResumePausedJobs}
|
||||
size="small"
|
||||
variant="ghost"
|
||||
title={pausedQueues.join(', ')}
|
||||
>
|
||||
<Text class="hidden md:block">
|
||||
{$t('resume_paused_jobs', { values: { count: pausedQueues.length } })}
|
||||
</Text>
|
||||
</Button>
|
||||
{/if}
|
||||
<HeaderButton action={CreateJob} />
|
||||
<HeaderButton action={ManageConcurrency} />
|
||||
</HStack>
|
||||
{/snippet}
|
||||
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]} actions={[ResumePaused, CreateJob, ManageConcurrency]}>
|
||||
<section id="setting-content" class="flex place-content-center sm:mx-4">
|
||||
<section class="w-full pb-28 sm:w-5/6 md:w-212.5">
|
||||
{#if queues}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
<script lang="ts">
|
||||
import HeaderButton from '$lib/components/HeaderButton.svelte';
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import QueueGraph from '$lib/components/QueueGraph.svelte';
|
||||
@@ -7,7 +6,18 @@
|
||||
import { queueManager } from '$lib/managers/queue-manager.svelte';
|
||||
import { asQueueItem, getQueueActions } from '$lib/services/queue.service';
|
||||
import { type QueueResponseDto } from '@immich/sdk';
|
||||
import { Badge, Card, CardBody, CardHeader, CardTitle, Container, Heading, HStack, Icon, Text } from '@immich/ui';
|
||||
import {
|
||||
Badge,
|
||||
Card,
|
||||
CardBody,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
Container,
|
||||
Heading,
|
||||
Icon,
|
||||
MenuItemType,
|
||||
Text,
|
||||
} from '@immich/ui';
|
||||
import { mdiClockTimeTwoOutline } from '@mdi/js';
|
||||
import { onMount } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
@@ -35,15 +45,10 @@
|
||||
|
||||
<OnEvents {onQueueUpdate} />
|
||||
|
||||
<AdminPageLayout breadcrumbs={[{ title: $t('admin.queues'), href: AppRoute.ADMIN_QUEUES }, { title: item.title }]}>
|
||||
{#snippet buttons()}
|
||||
<HStack gap={0}>
|
||||
<HeaderButton action={Pause} />
|
||||
<HeaderButton action={Resume} />
|
||||
<HeaderButton action={Empty} />
|
||||
<HeaderButton action={RemoveFailedJobs} />
|
||||
</HStack>
|
||||
{/snippet}
|
||||
<AdminPageLayout
|
||||
breadcrumbs={[{ title: $t('admin.queues'), href: AppRoute.ADMIN_QUEUES }, { title: item.title }]}
|
||||
actions={[Pause, Resume, Empty, MenuItemType.Divider, RemoveFailedJobs]}
|
||||
>
|
||||
<div>
|
||||
<Container size="large" center>
|
||||
<div class="mb-1 mt-4 flex items-center gap-2">
|
||||
|
||||
92
web/src/routes/admin/system-settings/+layout.svelte
Normal file
92
web/src/routes/admin/system-settings/+layout.svelte
Normal file
@@ -0,0 +1,92 @@
|
||||
<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, Card, CardHeader, CardTitle, CommandPaletteContext, Container, Icon, Text } from '@immich/ui';
|
||||
import { mdiPencilOutline } from '@mdi/js';
|
||||
import type { Snippet } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
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('');
|
||||
const filteredGroups = $derived(
|
||||
settings
|
||||
.map(({ title, items }) => {
|
||||
const query = searchQuery.toLowerCase();
|
||||
return {
|
||||
title,
|
||||
items: items.filter(
|
||||
({ title, subtitle }) => title.toLowerCase().includes(query) || subtitle.toLowerCase().includes(query),
|
||||
),
|
||||
};
|
||||
})
|
||||
.filter(({ items }) => items.length > 0),
|
||||
);
|
||||
</script>
|
||||
|
||||
<CommandPaletteContext commands={[CopyToClipboard, Upload, Download]} />
|
||||
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]} actions={[CopyToClipboard, Download, Upload]}>
|
||||
<section class="flex place-content-center sm:px-4 mt-4">
|
||||
<section class="w-full pb-28">
|
||||
<Container size="medium" center>
|
||||
{#if featureFlagsManager.value.configFile}
|
||||
<Alert color="warning" class="text-dark my-4" title={$t('admin.config_set_by_file')} />
|
||||
{/if}
|
||||
|
||||
<div class="mb-4">
|
||||
<SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-8">
|
||||
{#each filteredGroups as { title, items } (title)}
|
||||
<div>
|
||||
<Card color="secondary">
|
||||
<CardHeader class="px-5 py-3">
|
||||
<CardTitle>
|
||||
<Text color="primary" fontWeight="semi-bold">{title}</Text>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<div>
|
||||
{#each items as { title, subtitle, href, icon }, i (i)}
|
||||
<Button
|
||||
variant="outline"
|
||||
shape="rectangle"
|
||||
color="secondary"
|
||||
class="flex justify-between border-subtle"
|
||||
{href}
|
||||
>
|
||||
<div class="flex flex-col items-start">
|
||||
<Text fontWeight="semi-bold" class="flex items-center gap-2">
|
||||
<!-- <Icon {icon} /> -->
|
||||
{title}
|
||||
</Text>
|
||||
<Text class="line-clamp-1" color="muted">{subtitle}</Text>
|
||||
</div>
|
||||
<Icon icon={mdiPencilOutline} size="1.25rem" />
|
||||
</Button>
|
||||
{/each}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Container>
|
||||
</section>
|
||||
</section>
|
||||
</AdminPageLayout>
|
||||
|
||||
{@render children?.()}
|
||||
@@ -1,17 +1,12 @@
|
||||
import { authenticate } from '$lib/utils/auth';
|
||||
import { getFormatter } from '$lib/utils/i18n';
|
||||
import { getConfig, getConfigDefaults } from '@immich/sdk';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ url }) => {
|
||||
await authenticate(url, { admin: true });
|
||||
const config = await getConfig();
|
||||
const defaultConfig = await getConfigDefaults();
|
||||
const $t = await getFormatter();
|
||||
|
||||
return {
|
||||
config,
|
||||
defaultConfig,
|
||||
meta: {
|
||||
title: $t('admin.system_settings'),
|
||||
},
|
||||
@@ -1,249 +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 HeaderButton from '$lib/components/HeaderButton.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, HStack } 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 }]}>
|
||||
{#snippet buttons()}
|
||||
<HStack gap={1}>
|
||||
<div class="hidden lg:block">
|
||||
<SearchBar placeholder={$t('search_settings')} bind:name={searchQuery} showLoadingSpinner={false} />
|
||||
</div>
|
||||
<HeaderButton action={CopyToClipboard} />
|
||||
<HeaderButton action={Download} />
|
||||
<HeaderButton action={Upload} />
|
||||
</HStack>
|
||||
{/snippet}
|
||||
|
||||
<section id="setting-content" class="flex place-content-center sm:mx-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="block lg:hidden">
|
||||
<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>
|
||||
|
||||
218
web/src/routes/admin/system-settings/authentication/+page.svelte
Normal file
218
web/src/routes/admin/system-settings/authentication/+page.svelte
Normal file
@@ -0,0 +1,218 @@
|
||||
<script lang="ts">
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import SystemSettingsCard from '$lib/components/SystemSettingsCard.svelte';
|
||||
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, Field, Input, modalManager, NumberInput, Switch, 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')}>
|
||||
<Field label={$t('admin.password_enable_description')} {disabled}>
|
||||
<Switch bind:checked={configToEdit.passwordLogin.enabled} />
|
||||
</Field>
|
||||
</SystemSettingsCard>
|
||||
|
||||
<SystemSettingsCard title={$t('admin.oauth_settings')} subtitle={$t('admin.oauth_settings_description')}>
|
||||
<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>
|
||||
|
||||
<Field label={$t('admin.oauth_enable_description')} {disabled}>
|
||||
<Switch bind:checked={configToEdit.oauth.enabled} />
|
||||
</Field>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Text size="small">{$t('admin.unlink_all_oauth_accounts_description')}</Text>
|
||||
<div>
|
||||
<Button size="small" color="secondary" onclick={handleUnlinkAllOAuthAccounts}
|
||||
>{$t('admin.unlink_all_oauth_accounts')}</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Field label="ISSUER_URL" required disabled={disabled || !configToEdit.oauth.enabled}>
|
||||
<Input bind:value={configToEdit.oauth.issuerUrl} />
|
||||
</Field>
|
||||
|
||||
<Field label="CLIENT_ID" required disabled={disabled || !configToEdit.oauth.enabled}>
|
||||
<Input bind:value={configToEdit.oauth.clientId} />
|
||||
</Field>
|
||||
|
||||
<Field label="CLIENT_SECRET" required disabled={disabled || !configToEdit.oauth.enabled}>
|
||||
<Input bind:value={configToEdit.oauth.clientSecret} />
|
||||
</Field>
|
||||
|
||||
{#if configToEdit.oauth.clientSecret}
|
||||
<SettingSelect
|
||||
label="TOKEN_ENDPOINT_AUTH_METHOD"
|
||||
bind:value={configToEdit.oauth.tokenEndpointAuthMethod}
|
||||
disabled={disabled || !configToEdit.oauth.enabled || !configToEdit.oauth.clientSecret}
|
||||
isEdited={!(configToEdit.oauth.tokenEndpointAuthMethod === config.oauth.tokenEndpointAuthMethod)}
|
||||
options={[
|
||||
{ value: OAuthTokenEndpointAuthMethod.ClientSecretPost, text: 'client_secret_post' },
|
||||
{ value: OAuthTokenEndpointAuthMethod.ClientSecretBasic, text: 'client_secret_basic' },
|
||||
]}
|
||||
name="tokenEndpointAuthMethod"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<Field label="SCOPE" required disabled={disabled || !configToEdit.oauth.enabled}>
|
||||
<Input bind:value={configToEdit.oauth.scope} />
|
||||
</Field>
|
||||
|
||||
<Field label="ID_TOKEN_SIGNED_RESPONSE_ALG" required disabled={disabled || !configToEdit.oauth.enabled}>
|
||||
<Input bind:value={configToEdit.oauth.signingAlgorithm} />
|
||||
</Field>
|
||||
|
||||
<Field label="USERINFO_SIGNED_RESPONSE_ALG" required disabled={disabled || !configToEdit.oauth.enabled}>
|
||||
<Input bind:value={configToEdit.oauth.profileSigningAlgorithm} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.oauth_timeout')}
|
||||
description={$t('admin.oauth_timeout_description')}
|
||||
required
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
>
|
||||
<NumberInput bind:value={configToEdit.oauth.timeout} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.oauth_storage_label_claim')}
|
||||
description={$t('admin.oauth_storage_label_claim_description')}
|
||||
required
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
>
|
||||
<Input bind:value={configToEdit.oauth.storageLabelClaim} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.oauth_role_claim')}
|
||||
description={$t('admin.oauth_role_claim_description')}
|
||||
required
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
>
|
||||
<Input bind:value={configToEdit.oauth.roleClaim} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.oauth_storage_quota_claim')}
|
||||
description={$t('admin.oauth_storage_quota_claim_description')}
|
||||
required
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
>
|
||||
<Input bind:value={configToEdit.oauth.storageQuotaClaim} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.oauth_storage_quota_default')}
|
||||
description={$t('admin.oauth_storage_quota_default_description')}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
>
|
||||
<NumberInput
|
||||
bind:value={
|
||||
() => configToEdit.oauth.defaultStorageQuota ?? undefined,
|
||||
(value) => (configToEdit.oauth.defaultStorageQuota = value ?? null)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label={$t('admin.oauth_button_text')} disabled={disabled || !configToEdit.oauth.enabled}>
|
||||
<Input bind:value={configToEdit.oauth.buttonText} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.oauth_auto_register')}
|
||||
description={$t('admin.oauth_auto_register_description')}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
>
|
||||
<Switch bind:checked={configToEdit.oauth.autoRegister} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.oauth_auto_launch')}
|
||||
description={$t('admin.oauth_auto_launch_description')}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
>
|
||||
<Switch bind:checked={configToEdit.oauth.autoLaunch} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.oauth_mobile_redirect_uri_override')}
|
||||
description={$t('admin.oauth_mobile_redirect_uri_override_description', {
|
||||
values: { callback: 'app.immich:///oauth-callback' },
|
||||
})}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
>
|
||||
<Switch
|
||||
bind:checked={configToEdit.oauth.mobileOverrideEnabled}
|
||||
onCheckedChange={() => handleToggleOverride(configToEdit)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{#if configToEdit.oauth.mobileOverrideEnabled}
|
||||
<Field
|
||||
label={$t('admin.oauth_mobile_redirect_uri')}
|
||||
required
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
>
|
||||
<Input bind:value={configToEdit.oauth.mobileRedirectUri} />
|
||||
</Field>
|
||||
{/if}
|
||||
</SystemSettingsCard>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
51
web/src/routes/admin/system-settings/backup/+page.svelte
Normal file
51
web/src/routes/admin/system-settings/backup/+page.svelte
Normal file
@@ -0,0 +1,51 @@
|
||||
<script lang="ts">
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import FormatMessage from '$lib/elements/FormatMessage.svelte';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { Field, HelperText, Input, Link, NumberInput, Switch } from '@immich/ui';
|
||||
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, configToEdit })}
|
||||
<Field label={$t('admin.backup_database_enable_description')} {disabled}>
|
||||
<Switch bind:checked={configToEdit.backup.database.enabled} />
|
||||
</Field>
|
||||
|
||||
<SettingSelect
|
||||
options={cronExpressionOptions}
|
||||
disabled={disabled || !configToEdit.backup.database.enabled}
|
||||
name="expression"
|
||||
label={$t('admin.cron_expression_presets')}
|
||||
bind:value={configToEdit.backup.database.cronExpression}
|
||||
/>
|
||||
|
||||
<Field label={$t('admin.cron_expression')} required disabled={disabled || !configToEdit.backup.database.enabled}>
|
||||
<Input bind:value={configToEdit.backup.database.cronExpression} />
|
||||
<HelperText>
|
||||
<FormatMessage key="admin.cron_expression_description">
|
||||
{#snippet children({ message })}
|
||||
<Link href="https://crontab.guru/#{configToEdit.backup.database.cronExpression.replaceAll(' ', '_')}">
|
||||
{message}
|
||||
</Link>
|
||||
{/snippet}
|
||||
</FormatMessage>
|
||||
</HelperText>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
required
|
||||
label={$t('admin.backup_keep_last_amount')}
|
||||
disabled={disabled || !configToEdit.backup.database.enabled}
|
||||
>
|
||||
<NumberInput bind:value={configToEdit.backup.database.keepLastAmount}></NumberInput>
|
||||
</Field>
|
||||
{/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>
|
||||
50
web/src/routes/admin/system-settings/job/+page.svelte
Normal file
50
web/src/routes/admin/system-settings/job/+page.svelte
Normal file
@@ -0,0 +1,50 @@
|
||||
<script lang="ts">
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { getQueueName } from '$lib/utils';
|
||||
import { QueueName, type SystemConfigDto, type SystemConfigJobDto } from '@immich/sdk';
|
||||
import { Field, HelperText, NumberInput } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
const queueNames = [
|
||||
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, configToEdit })}
|
||||
{#each queueNames as queueName (queueName)}
|
||||
{#if isSystemConfigJobDto(configToEdit, queueName)}
|
||||
<Field
|
||||
required
|
||||
{disabled}
|
||||
label={$t('admin.job_concurrency', { values: { job: $getQueueName(queueName) } })}
|
||||
description=""
|
||||
>
|
||||
<NumberInput bind:value={configToEdit.job[queueName].concurrency} />
|
||||
</Field>
|
||||
{:else}
|
||||
<Field label={$t('admin.job_concurrency', { values: { job: $getQueueName(queueName) } })}>
|
||||
<NumberInput value={1} disabled={true} />
|
||||
<HelperText>{$t('admin.job_not_concurrency_safe')}</HelperText>
|
||||
</Field>
|
||||
{/if}
|
||||
{/each}
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
17
web/src/routes/admin/system-settings/library/+page.svelte
Normal file
17
web/src/routes/admin/system-settings/library/+page.svelte
Normal file
@@ -0,0 +1,17 @@
|
||||
<script lang="ts">
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { Field, NumberInput } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['user']}>
|
||||
{#snippet child({ disabled, configToEdit })}
|
||||
<Field
|
||||
label={$t('admin.user_delete_delay_settings')}
|
||||
description={$t('admin.user_delete_delay_settings_description')}
|
||||
{disabled}
|
||||
>
|
||||
<NumberInput bind:value={configToEdit.user.deleteDelay} min={1} />
|
||||
</Field>
|
||||
{/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>
|
||||
32
web/src/routes/admin/system-settings/logging/+page.svelte
Normal file
32
web/src/routes/admin/system-settings/logging/+page.svelte
Normal file
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { LogLevel } from '@immich/sdk';
|
||||
import { Field, Switch } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['logging']}>
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<Field required {disabled} label={$t('admin.logging_enable_description')}>
|
||||
<Switch bind:checked={configToEdit.logging.enabled} />
|
||||
</Field>
|
||||
|
||||
<SettingSelect
|
||||
label={$t('level')}
|
||||
desc={$t('admin.logging_level_description')}
|
||||
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,281 @@
|
||||
<script lang="ts">
|
||||
import SystemSettingsCard from '$lib/components/SystemSettingsCard.svelte';
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
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,
|
||||
Field,
|
||||
HelperText,
|
||||
IconButton,
|
||||
Input,
|
||||
Label,
|
||||
Link,
|
||||
NumberInput,
|
||||
Stack,
|
||||
Switch,
|
||||
Text,
|
||||
} from '@immich/ui';
|
||||
import { mdiPlus, mdiTrashCanOutline } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['machineLearning']} size="large">
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<Field
|
||||
label={$t('admin.machine_learning_enabled')}
|
||||
description={$t('admin.machine_learning_enabled_description')}
|
||||
{disabled}
|
||||
>
|
||||
<Switch bind:checked={configToEdit.machineLearning.enabled} />
|
||||
</Field>
|
||||
|
||||
<Label label={$t('url')} />
|
||||
<Text size="small" color="muted">{$t('admin.machine_learning_url_description')}</Text>
|
||||
|
||||
<Stack>
|
||||
{#each configToEdit.machineLearning.urls as _, i (i)}
|
||||
<Input
|
||||
bind:value={configToEdit.machineLearning.urls[i]}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled}
|
||||
>
|
||||
{#snippet trailingIcon()}
|
||||
{#if configToEdit.machineLearning.urls.length > 1}
|
||||
<IconButton
|
||||
size="small"
|
||||
aria-label={$t('remove')}
|
||||
onclick={() => configToEdit.machineLearning.urls.splice(i, 1)}
|
||||
icon={mdiTrashCanOutline}
|
||||
color="danger"
|
||||
/>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</Input>
|
||||
{/each}
|
||||
</Stack>
|
||||
|
||||
<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')}
|
||||
>
|
||||
<Field
|
||||
label={$t('admin.machine_learning_availability_checks_enabled')}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled}
|
||||
>
|
||||
<Switch bind:checked={configToEdit.machineLearning.availabilityChecks.enabled} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.machine_learning_availability_checks_interval')}
|
||||
description={$t('admin.machine_learning_availability_checks_interval_description')}
|
||||
disabled={disabled ||
|
||||
!configToEdit.machineLearning.enabled ||
|
||||
!configToEdit.machineLearning.availabilityChecks.enabled}
|
||||
>
|
||||
<NumberInput bind:value={configToEdit.machineLearning.availabilityChecks.interval} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.machine_learning_availability_checks_timeout')}
|
||||
description={$t('admin.machine_learning_availability_checks_timeout_description')}
|
||||
disabled={disabled ||
|
||||
!configToEdit.machineLearning.enabled ||
|
||||
!configToEdit.machineLearning.availabilityChecks.enabled}
|
||||
>
|
||||
<NumberInput bind:value={configToEdit.machineLearning.availabilityChecks.timeout} />
|
||||
</Field>
|
||||
</SystemSettingsCard>
|
||||
|
||||
<SystemSettingsCard
|
||||
title={$t('admin.machine_learning_smart_search')}
|
||||
subtitle={$t('admin.machine_learning_smart_search_description')}
|
||||
>
|
||||
<Field
|
||||
label={$t('admin.machine_learning_smart_search_enabled')}
|
||||
description={$t('admin.machine_learning_smart_search_enabled_description')}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled}
|
||||
>
|
||||
<Switch bind:checked={configToEdit.machineLearning.clip.enabled} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.machine_learning_clip_model')}
|
||||
required={true}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.clip.enabled}
|
||||
>
|
||||
<Input bind:value={configToEdit.machineLearning.clip.modelName} />
|
||||
<HelperText>
|
||||
<FormatMessage key="admin.machine_learning_clip_model_description">
|
||||
{#snippet children({ message })}
|
||||
<Link href="https://huggingface.co/immich-app"><u>{message}</u></Link>
|
||||
{/snippet}
|
||||
</FormatMessage>
|
||||
</HelperText>
|
||||
</Field>
|
||||
</SystemSettingsCard>
|
||||
|
||||
<SystemSettingsCard
|
||||
title={$t('admin.machine_learning_duplicate_detection')}
|
||||
subtitle={$t('admin.machine_learning_duplicate_detection_setting_description')}
|
||||
>
|
||||
<Field
|
||||
label={$t('admin.machine_learning_duplicate_detection_enabled')}
|
||||
description={$t('admin.machine_learning_duplicate_detection_enabled_description')}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.clip.enabled}
|
||||
>
|
||||
<Switch bind:checked={configToEdit.machineLearning.duplicateDetection.enabled} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.machine_learning_max_detection_distance')}
|
||||
description={$t('admin.machine_learning_max_detection_distance_description')}
|
||||
disabled={disabled || !featureFlagsManager.value.duplicateDetection}
|
||||
>
|
||||
<NumberInput
|
||||
bind:value={configToEdit.machineLearning.duplicateDetection.maxDistance}
|
||||
step="0.0005"
|
||||
min={0.001}
|
||||
max={0.1}
|
||||
/>
|
||||
</Field>
|
||||
</SystemSettingsCard>
|
||||
|
||||
<SystemSettingsCard
|
||||
title={$t('admin.machine_learning_facial_recognition')}
|
||||
subtitle={$t('admin.machine_learning_facial_recognition_description')}
|
||||
>
|
||||
<Field
|
||||
label={$t('admin.machine_learning_facial_recognition_setting')}
|
||||
description={$t('admin.machine_learning_facial_recognition_setting_description')}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled}
|
||||
>
|
||||
<Switch bind:checked={configToEdit.machineLearning.facialRecognition.enabled} />
|
||||
</Field>
|
||||
|
||||
<SettingSelect
|
||||
label={$t('admin.machine_learning_facial_recognition_model')}
|
||||
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}
|
||||
/>
|
||||
|
||||
<Field
|
||||
label={$t('admin.machine_learning_min_detection_score')}
|
||||
description={$t('admin.machine_learning_min_detection_score_description')}
|
||||
disabled={disabled ||
|
||||
!configToEdit.machineLearning.enabled ||
|
||||
!configToEdit.machineLearning.facialRecognition.enabled}
|
||||
>
|
||||
<NumberInput
|
||||
bind:value={configToEdit.machineLearning.facialRecognition.minScore}
|
||||
step="0.01"
|
||||
min={0.1}
|
||||
max={1}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.machine_learning_max_recognition_distance')}
|
||||
description={$t('admin.machine_learning_max_recognition_distance_description')}
|
||||
disabled={disabled ||
|
||||
!configToEdit.machineLearning.enabled ||
|
||||
!configToEdit.machineLearning.facialRecognition.enabled}
|
||||
>
|
||||
<NumberInput
|
||||
bind:value={configToEdit.machineLearning.facialRecognition.maxDistance}
|
||||
step="0.01"
|
||||
min={0.1}
|
||||
max={2}
|
||||
/>
|
||||
</Field>
|
||||
<Field
|
||||
label={$t('admin.machine_learning_min_recognized_faces')}
|
||||
description={$t('admin.machine_learning_min_recognized_faces_description')}
|
||||
disabled={disabled ||
|
||||
!configToEdit.machineLearning.enabled ||
|
||||
!configToEdit.machineLearning.facialRecognition.enabled}
|
||||
>
|
||||
<NumberInput bind:value={configToEdit.machineLearning.facialRecognition.minFaces} step="1" min={1} />
|
||||
</Field>
|
||||
</SystemSettingsCard>
|
||||
|
||||
<SystemSettingsCard
|
||||
title={$t('admin.machine_learning_ocr')}
|
||||
subtitle={$t('admin.machine_learning_ocr_description')}
|
||||
>
|
||||
<Field
|
||||
label={$t('admin.machine_learning_ocr_enabled')}
|
||||
description={$t('admin.machine_learning_ocr_enabled_description')}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled}
|
||||
>
|
||||
<Switch bind:checked={configToEdit.machineLearning.ocr.enabled} />
|
||||
</Field>
|
||||
|
||||
<SettingSelect
|
||||
label={$t('admin.machine_learning_ocr_model')}
|
||||
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}
|
||||
/>
|
||||
|
||||
<Field
|
||||
label={$t('admin.machine_learning_ocr_min_detection_score')}
|
||||
description={$t('admin.machine_learning_ocr_min_detection_score_description')}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.ocr.enabled}
|
||||
>
|
||||
<NumberInput bind:value={configToEdit.machineLearning.ocr.minDetectionScore} step="0.1" min={0.1} max={1} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.machine_learning_ocr_min_recognition_score')}
|
||||
description={$t('admin.machine_learning_ocr_min_score_recognition_description')}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.ocr.enabled}
|
||||
>
|
||||
<NumberInput bind:value={configToEdit.machineLearning.ocr.minRecognitionScore} step="0.1" min={0.1} max={1} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.machine_learning_ocr_max_resolution')}
|
||||
description={$t('admin.machine_learning_ocr_max_resolution_description')}
|
||||
disabled={disabled || !configToEdit.machineLearning.enabled || !configToEdit.machineLearning.ocr.enabled}
|
||||
>
|
||||
<NumberInput bind:value={configToEdit.machineLearning.ocr.maxResolution} min={1} />
|
||||
</Field>
|
||||
</SystemSettingsCard>
|
||||
{/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,55 @@
|
||||
<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 { Field, Switch } from '@immich/ui';
|
||||
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)}
|
||||
/>
|
||||
<Field
|
||||
label={$t('admin.nightly_tasks_database_cleanup_setting')}
|
||||
description={$t('admin.nightly_tasks_database_cleanup_setting_description')}
|
||||
{disabled}
|
||||
>
|
||||
<Switch bind:checked={configToEdit.nightlyTasks.databaseCleanup} />
|
||||
</Field>
|
||||
<SettingSwitch
|
||||
title={$t('admin.nightly_tasks_missing_thumbnails_setting')}
|
||||
subtitle={$t('admin.nightly_tasks_missing_thumbnails_setting_description')}
|
||||
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>
|
||||
173
web/src/routes/admin/system-settings/oauth/+page.svelte
Normal file
173
web/src/routes/admin/system-settings/oauth/+page.svelte
Normal file
@@ -0,0 +1,173 @@
|
||||
<script lang="ts">
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import FormatMessage from '$lib/elements/FormatMessage.svelte';
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { handleUnlinkAllOAuthAccounts, onBeforeSave } from '$lib/services/system-config.service';
|
||||
import { OAuthTokenEndpointAuthMethod, type SystemConfigDto } from '@immich/sdk';
|
||||
import { Alert, Button, Field, Input, NumberInput, Switch, Text } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
const handleToggleOverride = (configToEdit: SystemConfigDto) => {
|
||||
// click runs before bind
|
||||
const previouslyEnabled = configToEdit.oauth.mobileOverrideEnabled;
|
||||
if (!previouslyEnabled && !configToEdit.oauth.mobileRedirectUri) {
|
||||
configToEdit.oauth.mobileRedirectUri = globalThis.location.origin + '/api/oauth/mobile-redirect';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['oauth']} size="large" {onBeforeSave}>
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<Alert color="warning">
|
||||
<div>test</div>
|
||||
<div class="flex flex-col gap-2 w-full">
|
||||
<Text size="small">{$t('admin.unlink_all_oauth_accounts_description')}</Text>
|
||||
<div class="flex justify-end">
|
||||
<Button size="small" variant="outline" color="warning" onclick={handleUnlinkAllOAuthAccounts}
|
||||
>{$t('admin.unlink_all_oauth_accounts')}</Button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</Alert>
|
||||
|
||||
<Text>
|
||||
<FormatMessage key="admin.oauth_settings_more_details">
|
||||
{#snippet children({ message })}
|
||||
<a href="https://docs.immich.app/administration/oauth" class="underline" target="_blank" rel="noreferrer">
|
||||
{message}
|
||||
</a>
|
||||
{/snippet}
|
||||
</FormatMessage>
|
||||
</Text>
|
||||
|
||||
<Field label={$t('admin.oauth_enable_description')} {disabled}>
|
||||
<Switch bind:checked={configToEdit.oauth.enabled} />
|
||||
</Field>
|
||||
|
||||
<Field label="ISSUER_URL" required disabled={disabled || !configToEdit.oauth.enabled}>
|
||||
<Input bind:value={configToEdit.oauth.issuerUrl} />
|
||||
</Field>
|
||||
|
||||
<Field label="CLIENT_ID" required disabled={disabled || !configToEdit.oauth.enabled}>
|
||||
<Input bind:value={configToEdit.oauth.clientId} />
|
||||
</Field>
|
||||
|
||||
<Field label="CLIENT_SECRET" required disabled={disabled || !configToEdit.oauth.enabled}>
|
||||
<Input bind:value={configToEdit.oauth.clientSecret} />
|
||||
</Field>
|
||||
|
||||
{#if configToEdit.oauth.clientSecret}
|
||||
<SettingSelect
|
||||
label="TOKEN_ENDPOINT_AUTH_METHOD"
|
||||
bind:value={configToEdit.oauth.tokenEndpointAuthMethod}
|
||||
disabled={disabled || !configToEdit.oauth.enabled || !configToEdit.oauth.clientSecret}
|
||||
isEdited={!(configToEdit.oauth.tokenEndpointAuthMethod === config.oauth.tokenEndpointAuthMethod)}
|
||||
options={[
|
||||
{ value: OAuthTokenEndpointAuthMethod.ClientSecretPost, text: 'client_secret_post' },
|
||||
{ value: OAuthTokenEndpointAuthMethod.ClientSecretBasic, text: 'client_secret_basic' },
|
||||
]}
|
||||
name="tokenEndpointAuthMethod"
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<Field label="SCOPE" required disabled={disabled || !configToEdit.oauth.enabled}>
|
||||
<Input bind:value={configToEdit.oauth.scope} />
|
||||
</Field>
|
||||
|
||||
<Field label="ID_TOKEN_SIGNED_RESPONSE_ALG" required disabled={disabled || !configToEdit.oauth.enabled}>
|
||||
<Input bind:value={configToEdit.oauth.signingAlgorithm} />
|
||||
</Field>
|
||||
|
||||
<Field label="USERINFO_SIGNED_RESPONSE_ALG" required disabled={disabled || !configToEdit.oauth.enabled}>
|
||||
<Input bind:value={configToEdit.oauth.profileSigningAlgorithm} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.oauth_timeout')}
|
||||
description={$t('admin.oauth_timeout_description')}
|
||||
required
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
>
|
||||
<NumberInput bind:value={configToEdit.oauth.timeout} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.oauth_storage_label_claim')}
|
||||
description={$t('admin.oauth_storage_label_claim_description')}
|
||||
required
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
>
|
||||
<Input bind:value={configToEdit.oauth.storageLabelClaim} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.oauth_role_claim')}
|
||||
description={$t('admin.oauth_role_claim_description')}
|
||||
required
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
>
|
||||
<Input bind:value={configToEdit.oauth.roleClaim} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.oauth_storage_quota_claim')}
|
||||
description={$t('admin.oauth_storage_quota_claim_description')}
|
||||
required
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
>
|
||||
<Input bind:value={configToEdit.oauth.storageQuotaClaim} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.oauth_storage_quota_default')}
|
||||
description={$t('admin.oauth_storage_quota_default_description')}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
>
|
||||
<NumberInput
|
||||
bind:value={
|
||||
() => configToEdit.oauth.defaultStorageQuota ?? undefined,
|
||||
(value) => (configToEdit.oauth.defaultStorageQuota = value ?? null)
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field label={$t('admin.oauth_button_text')} disabled={disabled || !configToEdit.oauth.enabled}>
|
||||
<Input bind:value={configToEdit.oauth.buttonText} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.oauth_auto_register')}
|
||||
description={$t('admin.oauth_auto_register_description')}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
>
|
||||
<Switch bind:checked={configToEdit.oauth.autoRegister} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.oauth_auto_launch')}
|
||||
description={$t('admin.oauth_auto_launch_description')}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
>
|
||||
<Switch bind:checked={configToEdit.oauth.autoLaunch} />
|
||||
</Field>
|
||||
|
||||
<Field
|
||||
label={$t('admin.oauth_mobile_redirect_uri_override')}
|
||||
description={$t('admin.oauth_mobile_redirect_uri_override_description', {
|
||||
values: { callback: 'app.immich:///oauth-callback' },
|
||||
})}
|
||||
disabled={disabled || !configToEdit.oauth.enabled}
|
||||
>
|
||||
<Switch
|
||||
bind:checked={configToEdit.oauth.mobileOverrideEnabled}
|
||||
onCheckedChange={() => handleToggleOverride(configToEdit)}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
{#if configToEdit.oauth.mobileOverrideEnabled}
|
||||
<Field label={$t('admin.oauth_mobile_redirect_uri')} required disabled={disabled || !configToEdit.oauth.enabled}>
|
||||
<Input bind:value={configToEdit.oauth.mobileRedirectUri} />
|
||||
</Field>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
14
web/src/routes/admin/system-settings/password/+page.svelte
Normal file
14
web/src/routes/admin/system-settings/password/+page.svelte
Normal file
@@ -0,0 +1,14 @@
|
||||
<script lang="ts">
|
||||
import SystemSettingsModal from '$lib/modals/SystemSettingsModal.svelte';
|
||||
import { onBeforeSave } from '$lib/services/system-config.service';
|
||||
import { Field, Switch } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['passwordLogin']} size="small" {onBeforeSave}>
|
||||
{#snippet child({ disabled, configToEdit })}
|
||||
<Field label={$t('admin.password_enable_description')} {disabled}>
|
||||
<Switch bind:checked={configToEdit.passwordLogin.enabled} />
|
||||
</Field>
|
||||
{/snippet}
|
||||
</SystemSettingsModal>
|
||||
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 { Field, Input } from '@immich/ui';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<SystemSettingsModal keys={['server']}>
|
||||
{#snippet child({ disabled, config, configToEdit })}
|
||||
<Field
|
||||
label={$t('admin.server_external_domain_settings')}
|
||||
description={$t('admin.server_external_domain_settings_description')}
|
||||
>
|
||||
<Input bind:value={configToEdit.server.externalDomain} />
|
||||
</Field>
|
||||
|
||||
<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>
|
||||
@@ -1,12 +1,11 @@
|
||||
<script lang="ts">
|
||||
import HeaderButton from '$lib/components/HeaderButton.svelte';
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import { getUserAdminsActions, handleNavigateUserAdmin } from '$lib/services/user-admin.service';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||
import { searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk';
|
||||
import { Button, CommandPaletteContext, HStack, Icon } from '@immich/ui';
|
||||
import { Button, CommandPaletteContext, Icon } from '@immich/ui';
|
||||
import { mdiInfinity } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
@@ -45,12 +44,7 @@
|
||||
|
||||
<CommandPaletteContext commands={[Create]} />
|
||||
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]}>
|
||||
{#snippet buttons()}
|
||||
<HStack gap={1}>
|
||||
<HeaderButton action={Create} />
|
||||
</HStack>
|
||||
{/snippet}
|
||||
<AdminPageLayout breadcrumbs={[{ title: data.meta.title }]} actions={[Create]}>
|
||||
<section id="setting-content" class="flex place-content-center sm:mx-4">
|
||||
<section class="w-full pb-28 lg:w-212.5">
|
||||
<table class="my-5 w-full text-start">
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import HeaderButton from '$lib/components/HeaderButton.svelte';
|
||||
import AdminPageLayout from '$lib/components/layouts/AdminPageLayout.svelte';
|
||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||
import ServerStatisticsCard from '$lib/components/server-statistics/ServerStatisticsCard.svelte';
|
||||
@@ -8,7 +7,6 @@
|
||||
import DeviceCard from '$lib/components/user-settings-page/device-card.svelte';
|
||||
import FeatureSetting from '$lib/components/users/FeatureSetting.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { serverConfigManager } from '$lib/managers/server-config-manager.svelte';
|
||||
import { getUserAdminActions } from '$lib/services/user-admin.service';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { createDateFormatter, findLocale } from '$lib/utils';
|
||||
@@ -26,8 +24,8 @@
|
||||
Container,
|
||||
getByteUnitString,
|
||||
Heading,
|
||||
HStack,
|
||||
Icon,
|
||||
MenuItemType,
|
||||
Stack,
|
||||
Text,
|
||||
} from '@immich/ui';
|
||||
@@ -42,15 +40,14 @@
|
||||
mdiPlayCircle,
|
||||
mdiTrashCanOutline,
|
||||
} from '@mdi/js';
|
||||
import { DateTime } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
interface Props {
|
||||
type Props = {
|
||||
data: PageData;
|
||||
}
|
||||
};
|
||||
|
||||
let { data }: Props = $props();
|
||||
const { data }: Props = $props();
|
||||
|
||||
let user = $derived(data.user);
|
||||
const userPreferences = $derived(data.userPreferences);
|
||||
@@ -94,9 +91,6 @@
|
||||
await goto(AppRoute.ADMIN_USERS);
|
||||
}
|
||||
};
|
||||
|
||||
const getDeleteDate = (deletedAt: string): Date =>
|
||||
DateTime.fromISO(deletedAt).plus({ days: serverConfigManager.value.userDeleteDelay }).toJSDate();
|
||||
</script>
|
||||
|
||||
<OnEvents
|
||||
@@ -110,19 +104,8 @@
|
||||
|
||||
<AdminPageLayout
|
||||
breadcrumbs={[{ title: $t('admin.user_management'), href: AppRoute.ADMIN_USERS }, { title: user.name }]}
|
||||
actions={[ResetPassword, ResetPinCode, Update, Restore, MenuItemType.Divider, Delete]}
|
||||
>
|
||||
{#snippet buttons()}
|
||||
<HStack gap={0}>
|
||||
<HeaderButton action={ResetPassword} />
|
||||
<HeaderButton action={ResetPinCode} />
|
||||
<HeaderButton action={Update} />
|
||||
<HeaderButton
|
||||
action={Restore}
|
||||
title={$t('admin.user_restore_scheduled_removal', { values: { date: getDeleteDate(user.deletedAt!) } })}
|
||||
/>
|
||||
<HeaderButton action={Delete} />
|
||||
</HStack>
|
||||
{/snippet}
|
||||
<div>
|
||||
<Container size="large" center>
|
||||
{#if user.deletedAt}
|
||||
|
||||
Reference in New Issue
Block a user