mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
feat(web): translations (#9854)
* First test * Added translation using Weblate (French) * Translated using Weblate (German) Currently translated at 100.0% (4 of 4 strings) Translation: immich/web Translate-URL: http://familie-mach.net/projects/immich/web/de/ * Translated using Weblate (French) Currently translated at 100.0% (4 of 4 strings) Translation: immich/web Translate-URL: http://familie-mach.net/projects/immich/web/fr/ * Further testing * Further testing * Translated using Weblate (German) Currently translated at 100.0% (18 of 18 strings) Translation: immich/web Translate-URL: http://familie-mach.net/projects/immich/web/de/ * Further work * Update string file. * More strings * Automatically changed strings * Add automatically translated german file for testing purposes * Fix merge-face-selector component * Make server stats strings uppercase * Fix uppercase string * Fix some strings in jobs-panel * Fix lower and uppercase strings. Add a few additional string. Fix a few unnecessary replacements * Update german test translations * Fix typo in locales file * Change string keys * Extract more strings * Extract and replace some more strings * Update testtranslationfile * Change translation keys * Fix rebase errors * Fix one more rebase error * Remove german translation file * Co-authored-by: Daniel Dietzler <danieldietzler@users.noreply.github.com> * chore: clean up translations * chore: add new line * fix formatting * chore: fixes * fix: loading and tests --------- Co-authored-by: root <root@Blacki> Co-authored-by: admin <admin@example.com> Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
This commit is contained in:
parent
a2bccf23c9
commit
f446bc8caa
177 changed files with 2779 additions and 1017 deletions
|
|
@ -5,6 +5,7 @@
|
|||
import { serverConfig } from '$lib/stores/server-config.store';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Checkbox from '$lib/components/elements/checkbox.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
|
||||
|
|
@ -31,7 +32,7 @@
|
|||
dispatch('success');
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to delete user');
|
||||
handleError(error, $t('errors.unable_to_delete_user'));
|
||||
dispatch('fail');
|
||||
}
|
||||
};
|
||||
|
|
@ -43,8 +44,8 @@
|
|||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
title="Delete user"
|
||||
confirmText={forceDelete ? 'Permanently Delete' : 'Delete'}
|
||||
title={$t('delete_user')}
|
||||
confirmText={forceDelete ? $t('permanently_delete') : $t('delete')}
|
||||
onConfirm={handleDeleteUser}
|
||||
onCancel={() => dispatch('cancel')}
|
||||
disabled={deleteButtonDisabled}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
import JobTileButton from './job-tile-button.svelte';
|
||||
import JobTileStatus from './job-tile-status.svelte';
|
||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let title: string;
|
||||
export let subtitle: string | undefined;
|
||||
|
|
@ -43,9 +44,9 @@
|
|||
>
|
||||
<div class="flex w-full flex-col">
|
||||
{#if queueStatus.isPaused}
|
||||
<JobTileStatus color="warning">Paused</JobTileStatus>
|
||||
<JobTileStatus color="warning">{$t('paused')}</JobTileStatus>
|
||||
{:else if queueStatus.isActive}
|
||||
<JobTileStatus color="success">Active</JobTileStatus>
|
||||
<JobTileStatus color="success">{$t('active')}</JobTileStatus>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-2 p-5 sm:p-7 md:p-9">
|
||||
<div class="flex items-center gap-4 text-xl font-semibold text-immich-primary dark:text-immich-dark-primary">
|
||||
|
|
@ -63,7 +64,7 @@
|
|||
<CircleIconButton
|
||||
color="primary"
|
||||
icon={mdiClose}
|
||||
title="Clear message"
|
||||
title={$t('clear_message')}
|
||||
size="12"
|
||||
padding="1"
|
||||
on:click={() => dispatch('command', { command: JobCommand.ClearFailed, force: false })}
|
||||
|
|
@ -95,7 +96,7 @@
|
|||
<div
|
||||
class="{commonClasses} rounded-t-lg bg-immich-primary text-white dark:bg-immich-dark-primary dark:text-immich-dark-gray sm:rounded-l-lg sm:rounded-r-none"
|
||||
>
|
||||
<p>Active</p>
|
||||
<p>{$t('active')}</p>
|
||||
<p class="text-2xl">
|
||||
{jobCounts.active.toLocaleString($locale)}
|
||||
</p>
|
||||
|
|
@ -107,7 +108,7 @@
|
|||
<p class="text-2xl">
|
||||
{waitingCount.toLocaleString($locale)}
|
||||
</p>
|
||||
<p>Waiting</p>
|
||||
<p>{$t('waiting')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@
|
|||
import JobTile from './job-tile.svelte';
|
||||
import StorageMigrationDescription from './storage-migration-description.svelte';
|
||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let jobs: AllJobStatusResponseDto;
|
||||
|
||||
|
|
@ -60,38 +61,38 @@
|
|||
[JobName.ThumbnailGeneration]: {
|
||||
icon: mdiFileJpgBox,
|
||||
title: getJobName(JobName.ThumbnailGeneration),
|
||||
subtitle: 'Generate large, small and blurred thumbnails for each asset, as well as thumbnails for each person',
|
||||
subtitle: $t('thumbnail_generation_job_description'),
|
||||
},
|
||||
[JobName.MetadataExtraction]: {
|
||||
icon: mdiTable,
|
||||
title: getJobName(JobName.MetadataExtraction),
|
||||
subtitle: 'Extract metadata information from each asset, such as GPS and resolution',
|
||||
subtitle: $t('metadata_extraction_job_description'),
|
||||
},
|
||||
[JobName.Library]: {
|
||||
icon: mdiLibraryShelves,
|
||||
title: getJobName(JobName.Library),
|
||||
subtitle: 'Perform library tasks',
|
||||
allText: 'ALL',
|
||||
missingText: 'REFRESH',
|
||||
subtitle: $t('perform_library_tasks'),
|
||||
allText: $t('all').toUpperCase(),
|
||||
missingText: $t('refresh').toUpperCase(),
|
||||
},
|
||||
[JobName.Sidecar]: {
|
||||
title: getJobName(JobName.Sidecar),
|
||||
icon: mdiFileXmlBox,
|
||||
subtitle: 'Discover or synchronize sidecar metadata from the filesystem',
|
||||
allText: 'SYNC',
|
||||
missingText: 'DISCOVER',
|
||||
subtitle: $t('sidecar_job_description'),
|
||||
allText: $t('sync').toUpperCase(),
|
||||
missingText: $t('discover').toUpperCase(),
|
||||
disabled: !$featureFlags.sidecar,
|
||||
},
|
||||
[JobName.SmartSearch]: {
|
||||
icon: mdiImageSearch,
|
||||
title: getJobName(JobName.SmartSearch),
|
||||
subtitle: 'Run machine learning on assets to support smart search',
|
||||
subtitle: $t('smart_search_job_description'),
|
||||
disabled: !$featureFlags.smartSearch,
|
||||
},
|
||||
[JobName.DuplicateDetection]: {
|
||||
icon: mdiContentDuplicate,
|
||||
title: getJobName(JobName.DuplicateDetection),
|
||||
subtitle: 'Run machine learning on assets to detect similar images. Relies on Smart Search',
|
||||
subtitle: $t('duplicate_detection_job_description'),
|
||||
disabled: !$featureFlags.duplicateDetection,
|
||||
},
|
||||
[JobName.FaceDetection]: {
|
||||
|
|
@ -113,7 +114,7 @@
|
|||
[JobName.VideoConversion]: {
|
||||
icon: mdiVideo,
|
||||
title: getJobName(JobName.VideoConversion),
|
||||
subtitle: 'Transcode videos for wider compatibility with browsers and devices',
|
||||
subtitle: $t('video_conversion_job_description'),
|
||||
},
|
||||
[JobName.StorageTemplateMigration]: {
|
||||
icon: mdiFolderMove,
|
||||
|
|
@ -124,7 +125,7 @@
|
|||
[JobName.Migration]: {
|
||||
icon: mdiFolderMove,
|
||||
title: getJobName(JobName.Migration),
|
||||
subtitle: 'Migrate thumbnails for assets and faces to the latest folder structure',
|
||||
subtitle: $t('migration_job_description'),
|
||||
allowForceCommand: false,
|
||||
},
|
||||
};
|
||||
|
|
@ -159,8 +160,8 @@
|
|||
{title}
|
||||
{disabled}
|
||||
{subtitle}
|
||||
allText={allText || 'ALL'}
|
||||
missingText={missingText || 'MISSING'}
|
||||
allText={allText || $t('all').toUpperCase()}
|
||||
missingText={missingText || $t('missing').toUpperCase()}
|
||||
{allowForceCommand}
|
||||
{jobCounts}
|
||||
{queueStatus}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<script lang="ts">
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
Apply the current
|
||||
<a href="{AppRoute.ADMIN_SETTINGS}?open=storageTemplate" class="text-immich-primary dark:text-immich-dark-primary"
|
||||
>Storage template</a
|
||||
>{$t('storage_template_settings')}</a
|
||||
>
|
||||
to previously uploaded assets
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { restoreUserAdmin, type UserResponseDto } from '@immich/sdk';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let user: UserResponseDto;
|
||||
|
||||
|
|
@ -21,15 +22,15 @@
|
|||
dispatch('fail');
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to restore user');
|
||||
handleError(error, $t('errors.unable_to_restore_user'));
|
||||
dispatch('fail');
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
title="Restore user"
|
||||
confirmText="Continue"
|
||||
title={$t('restore_user')}
|
||||
confirmText={$t('continue')}
|
||||
confirmColor="green"
|
||||
onConfirm={handleRestoreUser}
|
||||
onCancel={() => dispatch('cancel')}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
import type { ServerStatsResponseDto } from '@immich/sdk';
|
||||
import { mdiCameraIris, mdiChartPie, mdiPlayCircle } from '@mdi/js';
|
||||
import StatsCard from './stats-card.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let stats: ServerStatsResponseDto = {
|
||||
photos: 0,
|
||||
|
|
@ -27,19 +28,19 @@
|
|||
|
||||
<div class="flex flex-col gap-5">
|
||||
<div>
|
||||
<p class="text-sm dark:text-immich-dark-fg">TOTAL USAGE</p>
|
||||
<p class="text-sm dark:text-immich-dark-fg">{$t('total_usage').toUpperCase()}</p>
|
||||
|
||||
<div class="mt-5 hidden justify-between lg:flex">
|
||||
<StatsCard icon={mdiCameraIris} title="PHOTOS" value={stats.photos} />
|
||||
<StatsCard icon={mdiPlayCircle} title="VIDEOS" value={stats.videos} />
|
||||
<StatsCard icon={mdiChartPie} title="STORAGE" value={statsUsage} unit={statsUsageUnit} />
|
||||
<StatsCard icon={mdiCameraIris} title={$t('photos').toUpperCase()} value={stats.photos} />
|
||||
<StatsCard icon={mdiPlayCircle} title={$t('videos').toUpperCase()} value={stats.videos} />
|
||||
<StatsCard icon={mdiChartPie} title={$t('storage').toUpperCase()} value={statsUsage} unit={statsUsageUnit} />
|
||||
</div>
|
||||
<div class="mt-5 flex lg:hidden">
|
||||
<div class="flex flex-col justify-between rounded-3xl bg-immich-gray p-5 dark:bg-immich-dark-gray">
|
||||
<div class="flex flex-wrap gap-x-12">
|
||||
<div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
|
||||
<Icon path={mdiCameraIris} size="25" />
|
||||
<p>PHOTOS</p>
|
||||
<p>{$t('photos').toUpperCase()}</p>
|
||||
</div>
|
||||
|
||||
<div class="relative text-center font-mono text-2xl font-semibold">
|
||||
|
|
@ -51,7 +52,7 @@
|
|||
<div class="flex flex-wrap gap-x-12">
|
||||
<div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
|
||||
<Icon path={mdiPlayCircle} size="25" />
|
||||
<p>VIDEOS</p>
|
||||
<p>{$t('videos').toUpperCase()}</p>
|
||||
</div>
|
||||
|
||||
<div class="relative text-center font-mono text-2xl font-semibold">
|
||||
|
|
@ -63,7 +64,7 @@
|
|||
<div class="flex flex-wrap gap-x-7">
|
||||
<div class="flex place-items-center gap-4 text-immich-primary dark:text-immich-dark-primary">
|
||||
<Icon path={mdiChartPie} size="25" />
|
||||
<p>STORAGE</p>
|
||||
<p>{$t('storage').toUpperCase()}</p>
|
||||
</div>
|
||||
|
||||
<div class="relative flex text-center font-mono text-2xl font-semibold">
|
||||
|
|
@ -78,16 +79,16 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm dark:text-immich-dark-fg">USER USAGE DETAIL</p>
|
||||
<p class="text-sm dark:text-immich-dark-fg">{$t('user_usage_detail').toUpperCase()}</p>
|
||||
<table class="mt-5 w-full text-left">
|
||||
<thead
|
||||
class="mb-4 flex h-12 w-full rounded-md border bg-gray-50 text-immich-primary dark:border-immich-dark-gray dark:bg-immich-dark-gray dark:text-immich-dark-primary"
|
||||
>
|
||||
<tr class="flex w-full place-items-center">
|
||||
<th class="w-1/4 text-center text-sm font-medium">User</th>
|
||||
<th class="w-1/4 text-center text-sm font-medium">Photos</th>
|
||||
<th class="w-1/4 text-center text-sm font-medium">Videos</th>
|
||||
<th class="w-1/4 text-center text-sm font-medium">Usage</th>
|
||||
<th class="w-1/4 text-center text-sm font-medium">{$t('user')}</th>
|
||||
<th class="w-1/4 text-center text-sm font-medium">{$t('photos')}</th>
|
||||
<th class="w-1/4 text-center text-sm font-medium">{$t('videos')}</th>
|
||||
<th class="w-1/4 text-center text-sm font-medium">{$t('usage')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
import { cloneDeep } from 'lodash-es';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
import type { SettingsEventType } from './admin-settings';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let config: SystemConfigDto;
|
||||
|
||||
|
|
@ -34,13 +35,13 @@
|
|||
|
||||
config = cloneDeep(newConfig);
|
||||
savedConfig = cloneDeep(newConfig);
|
||||
notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
|
||||
notificationController.show({ message: $t('settings_saved'), type: NotificationType.Info });
|
||||
|
||||
await loadConfig();
|
||||
|
||||
dispatch('save');
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save settings');
|
||||
handleError(error, $t('errors.unable_to_save_settings'));
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -63,7 +64,7 @@
|
|||
}
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset settings to default',
|
||||
message: $t('reset_settings_to_default'),
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
import { createEventDispatcher } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import type { SettingsEventType } from '../admin-settings';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -42,7 +43,11 @@
|
|||
</script>
|
||||
|
||||
{#if isConfirmOpen}
|
||||
<ConfirmDialog title="Disable login" onCancel={() => (isConfirmOpen = false)} onConfirm={() => handleSave(true)}>
|
||||
<ConfirmDialog
|
||||
title={$t('admin.disable_login')}
|
||||
onCancel={() => (isConfirmOpen = false)}
|
||||
onConfirm={() => handleSave(true)}
|
||||
>
|
||||
<svelte:fragment slot="prompt">
|
||||
<div class="flex flex-col gap-4">
|
||||
<p>Are you sure you want to disable all login methods? Login will be completely disabled.</p>
|
||||
|
|
@ -66,7 +71,11 @@
|
|||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingAccordion key="oauth" title="OAuth" subtitle="Manage OAuth login settings">
|
||||
<SettingAccordion
|
||||
key="oauth"
|
||||
title={$t('admin.oauth_settings')}
|
||||
subtitle={$t('admin.oauth_settings_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
For more details about this feature, refer to the <a
|
||||
|
|
@ -77,13 +86,18 @@
|
|||
>.
|
||||
</p>
|
||||
|
||||
<SettingSwitch {disabled} title="ENABLE" subtitle="Login with OAuth" bind:checked={config.oauth.enabled} />
|
||||
<SettingSwitch
|
||||
{disabled}
|
||||
title={$t('enable').toUpperCase()}
|
||||
subtitle={$t('admin.oauth_enable_description')}
|
||||
bind:checked={config.oauth.enabled}
|
||||
/>
|
||||
|
||||
{#if config.oauth.enabled}
|
||||
<hr />
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="ISSUER URL"
|
||||
label={$t('admin.oauth_issuer_url').toUpperCase()}
|
||||
bind:value={config.oauth.issuerUrl}
|
||||
required={true}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
|
|
@ -92,7 +106,7 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="CLIENT ID"
|
||||
label={$t('admin.oauth_client_id').toUpperCase()}
|
||||
bind:value={config.oauth.clientId}
|
||||
required={true}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
|
|
@ -101,7 +115,7 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="CLIENT SECRET"
|
||||
label={$t('admin.oauth_client_secret').toUpperCase()}
|
||||
bind:value={config.oauth.clientSecret}
|
||||
required={true}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
|
|
@ -110,7 +124,7 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="SCOPE"
|
||||
label={$t('admin.oauth_scope').toUpperCase()}
|
||||
bind:value={config.oauth.scope}
|
||||
required={true}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
|
|
@ -119,7 +133,7 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="SIGNING ALGORITHM"
|
||||
label={$t('admin.oauth_signing_algorithm').toUpperCase()}
|
||||
bind:value={config.oauth.signingAlgorithm}
|
||||
required={true}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
|
|
@ -128,8 +142,8 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="STORAGE LABEL CLAIM"
|
||||
desc="Automatically set the user's storage label to the value of this claim."
|
||||
label={$t('admin.oauth_storage_label_claim').toUpperCase()}
|
||||
desc={$t('admin.oauth_storage_label_claim_description')}
|
||||
bind:value={config.oauth.storageLabelClaim}
|
||||
required={true}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
|
|
@ -138,8 +152,8 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="STORAGE QUOTA CLAIM"
|
||||
desc="Automatically set the user's storage quota to the value of this claim."
|
||||
label={$t('admin.oauth_storage_quota_claim').toUpperCase()}
|
||||
desc={$t('admin.oauth_storage_quota_claim_description')}
|
||||
bind:value={config.oauth.storageQuotaClaim}
|
||||
required={true}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
|
|
@ -148,8 +162,8 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="DEFAULT STORAGE QUOTA (GiB)"
|
||||
desc="Quota in GiB to be used when no claim is provided (Enter 0 for unlimited quota)."
|
||||
label={$t('admin.oauth_storage_quota_default').toUpperCase()}
|
||||
desc={$t('admin.oauth_storage_quota_default_description')}
|
||||
bind:value={config.oauth.defaultStorageQuota}
|
||||
required={true}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
|
|
@ -158,7 +172,7 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="BUTTON TEXT"
|
||||
label={$t('admin.oauth_button_text').toUpperCase()}
|
||||
bind:value={config.oauth.buttonText}
|
||||
required={false}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
|
|
@ -166,22 +180,22 @@
|
|||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="AUTO REGISTER"
|
||||
subtitle="Automatically register new users after signing in with OAuth"
|
||||
title={$t('admin.oauth_auto_register').toUpperCase()}
|
||||
subtitle={$t('admin.oauth_auto_register_description')}
|
||||
bind:checked={config.oauth.autoRegister}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="AUTO LAUNCH"
|
||||
subtitle="Start the OAuth login flow automatically upon navigating to the login page"
|
||||
title={$t('admin.oauth_auto_launch').toUpperCase()}
|
||||
subtitle={$t('admin.oauth_auto_launch_description')}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
bind:checked={config.oauth.autoLaunch}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="MOBILE REDIRECT URI OVERRIDE"
|
||||
subtitle="Enable when 'app.immich:/' is an invalid redirect URI."
|
||||
title={$t('admin.oauth_mobile_redirect_uri_override').toUpperCase()}
|
||||
subtitle={$t('admin.oauth_mobile_redirect_uri_override_description')}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
on:click={() => handleToggleOverride()}
|
||||
bind:checked={config.oauth.mobileOverrideEnabled}
|
||||
|
|
@ -190,7 +204,7 @@
|
|||
{#if config.oauth.mobileOverrideEnabled}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="MOBILE REDIRECT URI"
|
||||
label={$t('admin.oauth_mobile_redirect_uri').toUpperCase()}
|
||||
bind:value={config.oauth.mobileRedirectUri}
|
||||
required={true}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
|
|
@ -201,13 +215,17 @@
|
|||
</div>
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion key="password" title="Password" subtitle="Manage password login settings">
|
||||
<SettingAccordion
|
||||
key="password"
|
||||
title={$t('admin.password_settings')}
|
||||
subtitle={$t('admin.password_settings_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ml-4 mt-4 flex flex-col">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
title={$t('enabled')}
|
||||
{disabled}
|
||||
subtitle="Login with email and password"
|
||||
subtitle={$t('admin.password_enable_description')}
|
||||
bind:checked={config.passwordLogin.enabled}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@
|
|||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import SettingCheckboxes from '$lib/components/shared-components/settings/setting-checkboxes.svelte';
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -42,7 +43,7 @@
|
|||
>H.264 codec</a
|
||||
>,
|
||||
<a href="https://trac.ffmpeg.org/wiki/Encode/H.265" class="underline" target="_blank" rel="noreferrer"
|
||||
>HEVC codec</a
|
||||
>{$t('admin.transcoding_hevc_codec')}</a
|
||||
>
|
||||
and
|
||||
<a href="https://trac.ffmpeg.org/wiki/Encode/VP9" class="underline" target="_blank" rel="noreferrer"
|
||||
|
|
@ -53,17 +54,17 @@
|
|||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
{disabled}
|
||||
label="CONSTANT RATE FACTOR (-crf)"
|
||||
desc="Video quality level. Typical values are 23 for H.264, 28 for HEVC, 31 for VP9, and 35 for AV1. Lower is better, but produces larger files."
|
||||
label={$t('admin.transcoding_constant_rate_factor')}
|
||||
desc={$t('admin.transcoding_constant_rate_factor_description')}
|
||||
bind:value={config.ffmpeg.crf}
|
||||
required={true}
|
||||
isEdited={config.ffmpeg.crf !== savedConfig.ffmpeg.crf}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="PRESET (-preset)"
|
||||
label={$t('admin.transcoding_preset_preset')}
|
||||
{disabled}
|
||||
desc="Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above faster."
|
||||
desc={$t('admin.transcoding_preset_preset_description')}
|
||||
bind:value={config.ffmpeg.preset}
|
||||
name="preset"
|
||||
options={[
|
||||
|
|
@ -81,9 +82,9 @@
|
|||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="AUDIO CODEC"
|
||||
label={$t('admin.transcoding_audio_codec').toUpperCase()}
|
||||
{disabled}
|
||||
desc="Opus is the highest quality option, but has lower compatibility with old devices or software."
|
||||
desc={$t('admin.transcoding_audio_codec_description')}
|
||||
bind:value={config.ffmpeg.targetAudioCodec}
|
||||
options={[
|
||||
{ value: AudioCodec.Aac, text: 'aac' },
|
||||
|
|
@ -99,9 +100,9 @@
|
|||
/>
|
||||
|
||||
<SettingCheckboxes
|
||||
label="ACCEPTED AUDIO CODECS"
|
||||
label={$t('admin.transcoding_accepted_audio_codecs').toUpperCase()}
|
||||
{disabled}
|
||||
desc="Select which audio codecs do not need to be transcoded. Only used for certain transcode policies."
|
||||
desc={$t('admin.transcoding_accepted_audio_codecs_description')}
|
||||
bind:value={config.ffmpeg.acceptedAudioCodecs}
|
||||
name="audioCodecs"
|
||||
options={[
|
||||
|
|
@ -113,9 +114,9 @@
|
|||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="VIDEO CODEC"
|
||||
label={$t('admin.transcoding_video_codec').toUpperCase()}
|
||||
{disabled}
|
||||
desc="VP9 has high efficiency and web compatibility, but takes longer to transcode. HEVC performs similarly, but has lower web compatibility. H.264 is widely compatible and quick to transcode, but produces much larger files. AV1 is the most efficient codec but lacks support on older devices."
|
||||
desc={$t('admin.transcoding_video_codec_description')}
|
||||
bind:value={config.ffmpeg.targetVideoCodec}
|
||||
options={[
|
||||
{ value: VideoCodec.H264, text: 'h264' },
|
||||
|
|
@ -129,9 +130,9 @@
|
|||
/>
|
||||
|
||||
<SettingCheckboxes
|
||||
label="ACCEPTED VIDEO CODECS"
|
||||
label={$t('admin.transcoding_accepted_video_codecs').toUpperCase()}
|
||||
{disabled}
|
||||
desc="Select which video codecs do not need to be transcoded. Only used for certain transcode policies."
|
||||
desc={$t('admin.transcoding_accepted_video_codecs_description')}
|
||||
bind:value={config.ffmpeg.acceptedVideoCodecs}
|
||||
name="videoCodecs"
|
||||
options={[
|
||||
|
|
@ -144,9 +145,9 @@
|
|||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="TARGET RESOLUTION"
|
||||
label={$t('admin.transcoding_target_resolution').toUpperCase()}
|
||||
{disabled}
|
||||
desc="Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
||||
desc={$t('admin.transcoding_target_resolution_description')}
|
||||
bind:value={config.ffmpeg.targetResolution}
|
||||
options={[
|
||||
{ value: '2160', text: '4k' },
|
||||
|
|
@ -163,8 +164,8 @@
|
|||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
{disabled}
|
||||
label="MAX BITRATE"
|
||||
desc="Setting a max bitrate can make file sizes more predictable at a minor cost to quality. At 720p, typical values are 2600k for VP9 or HEVC, or 4500k for H.264. Disabled if set to 0."
|
||||
label={$t('admin.transcoding_max_bitrate').toUpperCase()}
|
||||
desc={$t('admin.transcoding_max_bitrate_description')}
|
||||
bind:value={config.ffmpeg.maxBitrate}
|
||||
isEdited={config.ffmpeg.maxBitrate !== savedConfig.ffmpeg.maxBitrate}
|
||||
/>
|
||||
|
|
@ -172,44 +173,44 @@
|
|||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
{disabled}
|
||||
label="THREADS"
|
||||
desc="Higher values lead to faster encoding, but leave less room for the server to process other tasks while active. This value should not be more than the number of CPU cores. Maximizes utilization if set to 0."
|
||||
label={$t('admin.transcoding_threads').toUpperCase()}
|
||||
desc={$t('admin.transcoding_threads_description')}
|
||||
bind:value={config.ffmpeg.threads}
|
||||
isEdited={config.ffmpeg.threads !== savedConfig.ffmpeg.threads}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="TRANSCODE POLICY"
|
||||
label={$t('admin.transcoding_transcode_policy').toUpperCase()}
|
||||
{disabled}
|
||||
desc="Policy for when a video should be transcoded. HDR videos will always be transcoded (except if transcoding is disabled)."
|
||||
desc={$t('admin.transcoding_transcode_policy_description')}
|
||||
bind:value={config.ffmpeg.transcode}
|
||||
name="transcode"
|
||||
options={[
|
||||
{ value: TranscodePolicy.All, text: 'All videos' },
|
||||
{
|
||||
value: TranscodePolicy.Optimal,
|
||||
text: 'Videos higher than target resolution or not in an accepted format',
|
||||
text: $t('admin.transcoding_optimal_description'),
|
||||
},
|
||||
{
|
||||
value: TranscodePolicy.Bitrate,
|
||||
text: 'Videos higher than max bitrate or not in an accepted format',
|
||||
text: $t('admin.transcoding_bitrate_description'),
|
||||
},
|
||||
{
|
||||
value: TranscodePolicy.Required,
|
||||
text: 'Only videos not in an accepted format',
|
||||
text: $t('admin.transcoding_required_description'),
|
||||
},
|
||||
{
|
||||
value: TranscodePolicy.Disabled,
|
||||
text: "Don't transcode any videos, may break playback on some clients",
|
||||
text: $t('admin.transcoding_disabled_description'),
|
||||
},
|
||||
]}
|
||||
isEdited={config.ffmpeg.transcode !== savedConfig.ffmpeg.transcode}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="TONE-MAPPING"
|
||||
label={$t('admin.transcoding_tone_mapping').toUpperCase()}
|
||||
{disabled}
|
||||
desc="Attempts to preserve the appearance of HDR videos when converted to SDR. Each algorithm makes different tradeoffs for color, detail and brightness. Hable preserves detail, Mobius preserves color, and Reinhard preserves brightness."
|
||||
desc={$t('admin.transcoding_tone_mapping_description')}
|
||||
bind:value={config.ffmpeg.tonemap}
|
||||
name="tonemap"
|
||||
options={[
|
||||
|
|
@ -234,58 +235,58 @@
|
|||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="TWO-PASS ENCODING"
|
||||
title={$t('admin.transcoding_two_pass_encoding').toUpperCase()}
|
||||
{disabled}
|
||||
subtitle="Transcode in two passes to produce better encoded videos. When max bitrate is enabled (required for it to work with H.264 and HEVC), this mode uses a bitrate range based on the max bitrate and ignores CRF. For VP9, CRF can be used if max bitrate is disabled."
|
||||
subtitle={$t('admin.transcoding_two_pass_encoding_setting_description')}
|
||||
bind:checked={config.ffmpeg.twoPass}
|
||||
isEdited={config.ffmpeg.twoPass !== savedConfig.ffmpeg.twoPass}
|
||||
/>
|
||||
|
||||
<SettingAccordion
|
||||
key="hardware-acceleration"
|
||||
title="Hardware Acceleration"
|
||||
subtitle="Experimental; much faster, but will have lower quality at the same bitrate"
|
||||
title={$t('admin.transcoding_hardware_acceleration')}
|
||||
subtitle={$t('admin.transcoding_hardware_acceleration_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSelect
|
||||
label="ACCELERATION API"
|
||||
label={$t('admin.transcoding_acceleration_api').toUpperCase()}
|
||||
{disabled}
|
||||
desc="The API that will interact with your device to accelerate transcoding. This setting is 'best effort': it will fallback to software transcoding on failure. VP9 may or may not work depending on your hardware."
|
||||
desc={$t('admin.transcoding_acceleration_api_description')}
|
||||
bind:value={config.ffmpeg.accel}
|
||||
name="accel"
|
||||
options={[
|
||||
{ value: TranscodeHWAccel.Nvenc, text: 'NVENC (requires NVIDIA GPU)' },
|
||||
{ value: TranscodeHWAccel.Nvenc, text: $t('admin.transcoding_acceleration_nvenc') },
|
||||
{
|
||||
value: TranscodeHWAccel.Qsv,
|
||||
text: 'Quick Sync (requires 7th gen Intel CPU or later)',
|
||||
text: $t('admin.transcoding_acceleration_qsv'),
|
||||
},
|
||||
{
|
||||
value: TranscodeHWAccel.Vaapi,
|
||||
text: 'VAAPI',
|
||||
text: $t('admin.transcoding_acceleration_vaapi'),
|
||||
},
|
||||
{
|
||||
value: TranscodeHWAccel.Rkmpp,
|
||||
text: 'RKMPP (only on Rockchip SOCs)',
|
||||
text: $t('admin.transcoding_acceleration_rkmpp'),
|
||||
},
|
||||
{
|
||||
value: TranscodeHWAccel.Disabled,
|
||||
text: 'Disabled',
|
||||
text: $t('disabled'),
|
||||
},
|
||||
]}
|
||||
isEdited={config.ffmpeg.accel !== savedConfig.ffmpeg.accel}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="HARDWARE DECODING"
|
||||
title={$t('admin.transcoding_hardware_decoding').toUpperCase()}
|
||||
{disabled}
|
||||
subtitle="Applies only to NVENC and RKMPP. Enables end-to-end acceleration instead of only accelerating encoding. May not work on all videos."
|
||||
subtitle={$t('admin.transcoding_hardware_decoding_setting_description')}
|
||||
bind:checked={config.ffmpeg.accelDecode}
|
||||
isEdited={config.ffmpeg.accelDecode !== savedConfig.ffmpeg.accelDecode}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="CONSTANT QUALITY MODE"
|
||||
desc="ICQ is better than CQP, but some hardware acceleration devices do not support this mode. Setting this option will prefer the specified mode when using quality-based encoding. Ignored by NVENC as it does not support ICQ."
|
||||
label={$t('admin.transcoding_constant_quality_mode').toUpperCase()}
|
||||
desc={$t('admin.transcoding_constant_quality_mode_description')}
|
||||
bind:value={config.ffmpeg.cqMode}
|
||||
options={[
|
||||
{ value: CQMode.Auto, text: 'Auto' },
|
||||
|
|
@ -297,17 +298,17 @@
|
|||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="TEMPORAL AQ"
|
||||
title={$t('admin.transcoding_temporal_aq').toUpperCase()}
|
||||
{disabled}
|
||||
subtitle="Applies only to NVENC. Increases quality of high-detail, low-motion scenes. May not be compatible with older devices."
|
||||
subtitle={$t('admin.transcoding_temporal_aq_description')}
|
||||
bind:checked={config.ffmpeg.temporalAQ}
|
||||
isEdited={config.ffmpeg.temporalAQ !== savedConfig.ffmpeg.temporalAQ}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="PREFERRED HARDWARE DEVICE"
|
||||
desc="Applies only to VAAPI and QSV. Sets the dri node used for hardware transcoding."
|
||||
label={$t('admin.transcoding_preferred_hardware_device').toUpperCase()}
|
||||
desc={$t('admin.transcoding_preferred_hardware_device_description')}
|
||||
bind:value={config.ffmpeg.preferredHwDevice}
|
||||
isEdited={config.ffmpeg.preferredHwDevice !== savedConfig.ffmpeg.preferredHwDevice}
|
||||
{disabled}
|
||||
|
|
@ -317,14 +318,14 @@
|
|||
|
||||
<SettingAccordion
|
||||
key="advanced-options"
|
||||
title="Advanced"
|
||||
subtitle="Options most users should not need to change"
|
||||
title={$t('advanced')}
|
||||
subtitle={$t('admin.transcoding_advanced_options_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="TONE-MAPPING NPL"
|
||||
desc="Colors will be adjusted to look normal for a display of this brightness. Counter-intuitively, lower values increase the brightness of the video and vice versa since it compensates for the brightness of the display. 0 sets this value automatically."
|
||||
label={$t('admin.transcoding_tone_mapping_npl').toUpperCase()}
|
||||
desc={$t('admin.transcoding_tone_mapping_npl_description')}
|
||||
bind:value={config.ffmpeg.npl}
|
||||
isEdited={config.ffmpeg.npl !== savedConfig.ffmpeg.npl}
|
||||
{disabled}
|
||||
|
|
@ -332,8 +333,8 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="MAX B-FRAMES"
|
||||
desc="Higher values improve compression efficiency, but slow down encoding. May not be compatible with hardware acceleration on older devices. 0 disables B-frames, while -1 sets this value automatically."
|
||||
label={$t('admin.transcoding_max_b_frames').toUpperCase()}
|
||||
desc={$t('admin.transcoding_max_b_frames_description')}
|
||||
bind:value={config.ffmpeg.bframes}
|
||||
isEdited={config.ffmpeg.bframes !== savedConfig.ffmpeg.bframes}
|
||||
{disabled}
|
||||
|
|
@ -341,8 +342,8 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="REFERENCE FRAMES"
|
||||
desc="The number of frames to reference when compressing a given frame. Higher values improve compression efficiency, but slow down encoding. 0 sets this value automatically."
|
||||
label={$t('admin.transcoding_reference_frames').toUpperCase()}
|
||||
desc={$t('admin.transcoding_reference_frames_description')}
|
||||
bind:value={config.ffmpeg.refs}
|
||||
isEdited={config.ffmpeg.refs !== savedConfig.ffmpeg.refs}
|
||||
{disabled}
|
||||
|
|
@ -350,8 +351,8 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="MAX KEYFRAME INTERVAL"
|
||||
desc="Sets the maximum frame distance between keyframes. Lower values worsen compression efficiency, but improve seek times and may improve quality in scenes with fast movement. 0 sets this value automatically."
|
||||
label={$t('admin.transcoding_max_keyframe_interval').toUpperCase()}
|
||||
desc={$t('admin.transcoding_max_keyframe_interval_description')}
|
||||
bind:value={config.ffmpeg.gopSize}
|
||||
isEdited={config.ffmpeg.gopSize !== savedConfig.ffmpeg.gopSize}
|
||||
{disabled}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
import SettingInputField, {
|
||||
SettingInputFieldType,
|
||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -25,8 +26,8 @@
|
|||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSelect
|
||||
label="THUMBNAIL FORMAT"
|
||||
desc="WebP produces smaller files than JPEG, but is slower to encode."
|
||||
label={$t('admin.image_thumbnail_format').toUpperCase()}
|
||||
desc={$t('admin.image_format_description')}
|
||||
bind:value={config.image.thumbnailFormat}
|
||||
options={[
|
||||
{ value: ImageFormat.Jpeg, text: 'JPEG' },
|
||||
|
|
@ -38,8 +39,8 @@
|
|||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="THUMBNAIL RESOLUTION"
|
||||
desc="Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
||||
label={$t('admin.image_thumbnail_resolution').toUpperCase()}
|
||||
desc={$t('admin.image_thumbnail_resolution_description')}
|
||||
number
|
||||
bind:value={config.image.thumbnailSize}
|
||||
options={[
|
||||
|
|
@ -55,8 +56,8 @@
|
|||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="PREVIEW FORMAT"
|
||||
desc="WebP produces smaller files than JPEG, but is slower to encode."
|
||||
label={$t('admin.image_preview_format').toUpperCase()}
|
||||
desc={$t('admin.image_format_description')}
|
||||
bind:value={config.image.previewFormat}
|
||||
options={[
|
||||
{ value: ImageFormat.Jpeg, text: 'JPEG' },
|
||||
|
|
@ -68,8 +69,8 @@
|
|||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="PREVIEW RESOLUTION"
|
||||
desc="Used when viewing a single photo and for machine learning. Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
||||
label={$t('admin.image_preview_resolution').toUpperCase()}
|
||||
desc={$t('admin.image_preview_resolution_description')}
|
||||
number
|
||||
bind:value={config.image.previewSize}
|
||||
options={[
|
||||
|
|
@ -85,16 +86,16 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="QUALITY"
|
||||
desc="Image quality from 1-100. Higher is better for quality but produces larger files."
|
||||
label={$t('admin.image_quality').toUpperCase()}
|
||||
desc={$t('admin.image_quality_description')}
|
||||
bind:value={config.image.quality}
|
||||
isEdited={config.image.quality !== savedConfig.image.quality}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="PREFER WIDE GAMUT"
|
||||
subtitle="Use Display P3 for thumbnails. This better preserves the vibrance of images with wide colorspaces, but images may appear differently on old devices with an old browser version. sRGB images are kept as sRGB to avoid color shifts."
|
||||
title={$t('admin.image_prefer_wide_gamut').toUpperCase()}
|
||||
subtitle={$t('admin.image_prefer_wide_gamut_setting_description')}
|
||||
checked={config.image.colorspace === Colorspace.P3}
|
||||
on:toggle={(e) => (config.image.colorspace = e.detail ? Colorspace.P3 : Colorspace.Srgb)}
|
||||
isEdited={config.image.colorspace !== savedConfig.image.colorspace}
|
||||
|
|
@ -102,8 +103,8 @@
|
|||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="PREFER EMBEDDED PREVIEW"
|
||||
subtitle="Use embedded previews in RAW photos as the input to image processing when available. This can produce more accurate colors for some images, but the quality of the preview is camera-dependent and the image may have more compression artifacts."
|
||||
title={$t('admin.image_prefer_embedded_preview').toUpperCase()}
|
||||
subtitle={$t('admin.image_prefer_embedded_preview_setting_description')}
|
||||
checked={config.image.extractEmbedded}
|
||||
on:toggle={() => (config.image.extractEmbedded = !config.image.extractEmbedded)}
|
||||
isEdited={config.image.extractEmbedded !== savedConfig.image.extractEmbedded}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
} 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/setting-buttons-row.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -17,10 +18,10 @@
|
|||
export let disabled = false;
|
||||
|
||||
const cronExpressionOptions = [
|
||||
{ title: 'Every night at midnight', expression: '0 0 * * *' },
|
||||
{ title: 'Every night at 2am', expression: '0 2 * * *' },
|
||||
{ title: 'Every day at 1pm', expression: '0 13 * * *' },
|
||||
{ title: 'Every 6 hours', expression: '0 */6 * * *' },
|
||||
{ title: $t('interval.night_at_midnight'), expression: '0 0 * * *' },
|
||||
{ title: $t('interval.night_at_twoam'), expression: '0 2 * * *' },
|
||||
{ title: $t('interval.day_at_onepm'), expression: '0 13 * * *' },
|
||||
{ title: $t('interval.hours', { values: { hours: 6 } }), expression: '0 */6 * * *' },
|
||||
];
|
||||
|
||||
const dispatch = createEventDispatcher<SettingsEventType>();
|
||||
|
|
@ -30,16 +31,16 @@
|
|||
<div in:fade={{ duration: 500 }}>
|
||||
<SettingAccordion
|
||||
key="library-watching"
|
||||
title="Library watching (EXPERIMENTAL)"
|
||||
subtitle="Automatically watch for changed files"
|
||||
title={$t('admin.library_watching_settings')}
|
||||
subtitle={$t('admin.library_watching_settings_description')}
|
||||
isOpen
|
||||
>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="Watch filesystem"
|
||||
title={$t('enable')}
|
||||
{disabled}
|
||||
subtitle="Watch external libraries for file changes"
|
||||
subtitle={$t('admin.library_watching_enable_description')}
|
||||
bind:checked={config.library.watch.enabled}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -57,21 +58,21 @@
|
|||
|
||||
<SettingAccordion
|
||||
key="library-scanning"
|
||||
title="Periodic Scanning"
|
||||
subtitle="Configure periodic library scanning"
|
||||
title={$t('admin.library_scanning')}
|
||||
subtitle={$t('admin.library_scanning_description')}
|
||||
isOpen
|
||||
>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
title={$t('enabled').toUpperCase()}
|
||||
{disabled}
|
||||
subtitle="Enable periodic library scanning"
|
||||
subtitle={$t('admin.library_scanning_enable_description')}
|
||||
bind:checked={config.library.scan.enabled}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col my-2 dark:text-immich-dark-fg">
|
||||
<label class="text-sm" for="expression-select">Cron Expression Presets</label>
|
||||
<label class="text-sm" for="expression-select">{$t('admin.library_cron_expression_presets')}</label>
|
||||
<select
|
||||
class="p-2 mt-2 text-sm rounded-lg bg-slate-200 hover:cursor-pointer dark:bg-gray-600"
|
||||
disabled={disabled || !config.library.scan.enabled}
|
||||
|
|
@ -89,7 +90,7 @@
|
|||
inputType={SettingInputFieldType.TEXT}
|
||||
required={true}
|
||||
disabled={disabled || !config.library.scan.enabled}
|
||||
label="Cron Expression"
|
||||
label={$t('admin.library_cron_expression')}
|
||||
bind:value={config.library.scan.cronExpression}
|
||||
isEdited={config.library.scan.cronExpression !== savedConfig.library.scan.cronExpression}
|
||||
>
|
||||
|
|
@ -99,7 +100,7 @@
|
|||
href="https://crontab.guru"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer">Crontab Guru</a
|
||||
rel="noreferrer">{$t('crontab_guru')}</a
|
||||
>
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -20,10 +21,15 @@
|
|||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch title="ENABLED" {disabled} subtitle="Logging" bind:checked={config.logging.enabled} />
|
||||
<SettingSwitch
|
||||
title={$t('enabled').toUpperCase()}
|
||||
{disabled}
|
||||
subtitle={$t('admin.logging_enable_description')}
|
||||
bind:checked={config.logging.enabled}
|
||||
/>
|
||||
<SettingSelect
|
||||
label="LEVEL"
|
||||
desc="When enabled, what log level to use."
|
||||
label={$t('level').toUpperCase()}
|
||||
desc={$t('admin.logging_level_description')}
|
||||
bind:value={config.logging.level}
|
||||
options={[
|
||||
{ value: LogLevel.Fatal, text: 'Fatal' },
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { featureFlags } from '$lib/stores/server-config.store';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -26,8 +27,8 @@
|
|||
<form autocomplete="off" on:submit|preventDefault class="mx-4 mt-4">
|
||||
<div class="flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
subtitle="If disabled, all ML features will be disabled regardless of the below settings."
|
||||
title={$t('enabled').toUpperCase()}
|
||||
subtitle={$t('admin.machine_learning_enabled_description')}
|
||||
{disabled}
|
||||
bind:checked={config.machineLearning.enabled}
|
||||
/>
|
||||
|
|
@ -36,8 +37,8 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="URL"
|
||||
desc="URL of the machine learning server"
|
||||
label={$t('url').toUpperCase()}
|
||||
desc={$t('admin.machine_learning_url_description')}
|
||||
bind:value={config.machineLearning.url}
|
||||
required={true}
|
||||
disabled={disabled || !config.machineLearning.enabled}
|
||||
|
|
@ -47,13 +48,13 @@
|
|||
|
||||
<SettingAccordion
|
||||
key="smart-search"
|
||||
title="Smart Search"
|
||||
subtitle="Search for images semantically using CLIP embeddings"
|
||||
title={$t('admin.machine_learning_smart_search')}
|
||||
subtitle={$t('admin.machine_learning_smart_search_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
subtitle="If disabled, images will not be encoded for smart search."
|
||||
title={$t('enabled').toUpperCase()}
|
||||
subtitle={$t('admin.machine_learning_smart_search_enabled_description')}
|
||||
bind:checked={config.machineLearning.clip.enabled}
|
||||
disabled={disabled || !config.machineLearning.enabled}
|
||||
/>
|
||||
|
|
@ -62,7 +63,7 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="CLIP MODEL"
|
||||
label={$t('admin.machine_learning_clip_model').toUpperCase()}
|
||||
bind:value={config.machineLearning.clip.modelName}
|
||||
required={true}
|
||||
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.clip.enabled}
|
||||
|
|
@ -78,13 +79,13 @@
|
|||
|
||||
<SettingAccordion
|
||||
key="duplicate-detection"
|
||||
title="Duplicate Detection"
|
||||
subtitle="Use CLIP embeddings to find likely duplicates"
|
||||
title={$t('admin.machine_learning_duplicate_detection')}
|
||||
subtitle={$t('admin.machine_learning_duplicate_detection_setting_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
subtitle="If disabled, exactly identical assets will still be de-duplicated."
|
||||
title={$t('enabled').toUpperCase()}
|
||||
subtitle={$t('admin.machine_learning_duplicate_detection_enabled_description')}
|
||||
bind:checked={config.machineLearning.duplicateDetection.enabled}
|
||||
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.clip.enabled}
|
||||
/>
|
||||
|
|
@ -93,12 +94,12 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="MAX DETECTION DISTANCE"
|
||||
label={$t('admin.machine_learning_max_detection_distance').toUpperCase()}
|
||||
bind:value={config.machineLearning.duplicateDetection.maxDistance}
|
||||
step="0.0005"
|
||||
min={0.001}
|
||||
max={0.1}
|
||||
desc="Maximum distance between two images to consider them duplicates, ranging from 0.001-0.1. Higher values will detect more duplicates, but may result in false positives."
|
||||
desc={$t('admin.machine_learning_max_detection_distance_description')}
|
||||
disabled={disabled || !$featureFlags.duplicateDetection}
|
||||
isEdited={config.machineLearning.duplicateDetection.maxDistance !==
|
||||
savedConfig.machineLearning.duplicateDetection.maxDistance}
|
||||
|
|
@ -108,13 +109,13 @@
|
|||
|
||||
<SettingAccordion
|
||||
key="facial-recognition"
|
||||
title="Facial Recognition"
|
||||
subtitle="Detect, recognize and group faces in images"
|
||||
title={$t('admin.machine_learning_facial_recognition')}
|
||||
subtitle={$t('admin.machine_learning_facial_recognition_description')}
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
subtitle="If disabled, images will not be encoded for facial recognition and will not populate the People section in the Explore page."
|
||||
title={$t('enabled').toUpperCase()}
|
||||
subtitle={$t('admin.machine_learning_facial_recognition_setting_description')}
|
||||
bind:checked={config.machineLearning.facialRecognition.enabled}
|
||||
disabled={disabled || !config.machineLearning.enabled}
|
||||
/>
|
||||
|
|
@ -122,8 +123,8 @@
|
|||
<hr />
|
||||
|
||||
<SettingSelect
|
||||
label="FACIAL RECOGNITION MODEL"
|
||||
desc="Models are listed in descending order of size. Larger models are slower and use more memory, but produce better results. Note that you must re-run the Face Detection job for all images upon changing a model."
|
||||
label={$t('admin.machine_learning_facial_recognition_model').toUpperCase()}
|
||||
desc={$t('admin.machine_learning_facial_recognition_model_description')}
|
||||
name="facial-recognition-model"
|
||||
bind:value={config.machineLearning.facialRecognition.modelName}
|
||||
options={[
|
||||
|
|
@ -139,8 +140,8 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="MIN DETECTION SCORE"
|
||||
desc="Minimum confidence score for a face to be detected from 0-1. Lower values will detect more faces but may result in false positives."
|
||||
label={$t('admin.machine_learning_min_detection_score').toUpperCase()}
|
||||
desc={$t('admin.machine_learning_min_detection_score_description')}
|
||||
bind:value={config.machineLearning.facialRecognition.minScore}
|
||||
step="0.1"
|
||||
min={0}
|
||||
|
|
@ -152,8 +153,8 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="MAX RECOGNITION DISTANCE"
|
||||
desc="Maximum distance between two faces to be considered the same person, ranging from 0-2. Lowering this can prevent labeling two people as the same person, while raising it can prevent labeling the same person as two different people. Note that it is easier to merge two people than to split one person in two, so err on the side of a lower threshold when possible."
|
||||
label={$t('admin.machine_learning_max_recognition_distance').toUpperCase()}
|
||||
desc={$t('admin.machine_learning_max_recognition_distance_description')}
|
||||
bind:value={config.machineLearning.facialRecognition.maxDistance}
|
||||
step="0.1"
|
||||
min={0}
|
||||
|
|
@ -165,8 +166,8 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="MIN RECOGNIZED FACES"
|
||||
desc="The minimum number of recognized faces for a person to be created. Increasing this makes Facial Recognition more precise at the cost of increasing the chance that a face is not assigned to a person."
|
||||
label={$t('admin.machine_learning_min_recognized_faces').toUpperCase()}
|
||||
desc={$t('admin.machine_learning_min_recognized_faces_description')}
|
||||
bind:value={config.machineLearning.facialRecognition.minFaces}
|
||||
step="1"
|
||||
min={1}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
import SettingInputField, {
|
||||
SettingInputFieldType,
|
||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -23,12 +24,12 @@
|
|||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="flex flex-col gap-4">
|
||||
<SettingAccordion key="map" title="Map Settings" subtitle="Manage map settings">
|
||||
<SettingAccordion key="map" title={$t('admin.map_settings')} subtitle={$t('admin.map_settings_description')}>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
title={$t('enabled').toUpperCase()}
|
||||
{disabled}
|
||||
subtitle="Enable map features"
|
||||
subtitle={$t('admin.map_enable_description')}
|
||||
bind:checked={config.map.enabled}
|
||||
/>
|
||||
|
||||
|
|
@ -36,16 +37,16 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="Light Style"
|
||||
desc="URL to a style.json map theme"
|
||||
label={$t('admin.map_light_style')}
|
||||
desc={$t('admin.map_style_description')}
|
||||
bind:value={config.map.lightStyle}
|
||||
disabled={disabled || !config.map.enabled}
|
||||
isEdited={config.map.lightStyle !== savedConfig.map.lightStyle}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="Dark Style"
|
||||
desc="URL to a style.json map theme"
|
||||
label={$t('admin.map_dark_style')}
|
||||
desc={$t('admin.map_style_description')}
|
||||
bind:value={config.map.darkStyle}
|
||||
disabled={disabled || !config.map.enabled}
|
||||
isEdited={config.map.darkStyle !== savedConfig.map.darkStyle}
|
||||
|
|
@ -53,22 +54,22 @@
|
|||
</div></SettingAccordion
|
||||
>
|
||||
|
||||
<SettingAccordion key="reverse-geocoding" title="Reverse Geocoding Settings">
|
||||
<SettingAccordion key="reverse-geocoding" title={$t('admin.map_reverse_geocoding_settings')}>
|
||||
<svelte:fragment slot="subtitle">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
Manage <a
|
||||
href="https://immich.app/docs/features/reverse-geocoding"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer">Reverse Geocoding</a
|
||||
rel="noreferrer">{$t('admin.map_reverse_geocoding')}</a
|
||||
> settings
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
title={$t('enabled').toUpperCase()}
|
||||
{disabled}
|
||||
subtitle="Enable reverse geocoding"
|
||||
subtitle={$t('admin.map_reverse_geocoding_enable_description')}
|
||||
bind:checked={config.reverseGeocoding.enabled}
|
||||
/>
|
||||
</div></SettingAccordion
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import type { SettingsEventType } from '../admin-settings';
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -20,8 +21,8 @@
|
|||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
subtitle="Enable periodic requests to GitHub to check for new releases"
|
||||
title={$t('enabled').toUpperCase()}
|
||||
subtitle={$t('admin.version_check_enabled_description')}
|
||||
bind:checked={config.newVersionCheck.enabled}
|
||||
{disabled}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@
|
|||
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
||||
import SettingAccordion from '$lib/components/shared-components/settings/setting-accordion.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -23,11 +24,11 @@
|
|||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault class="mt-4">
|
||||
<div class="flex flex-col gap-4">
|
||||
<SettingAccordion key="email" title="Email" subtitle="Settings for sending email notifications">
|
||||
<SettingAccordion key="email" title={$t('email')} subtitle={$t('admin.notification_email_setting_description')}>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="Enabled"
|
||||
subtitle="Enable email notifications"
|
||||
title={$t('enabled')}
|
||||
subtitle={$t('admin.notification_enable_email_notifications')}
|
||||
{disabled}
|
||||
bind:checked={config.notifications.smtp.enabled}
|
||||
/>
|
||||
|
|
@ -37,8 +38,8 @@
|
|||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
required
|
||||
label="Host"
|
||||
desc="Host of the email server (e.g. smtp.immich.app)"
|
||||
label={$t('host')}
|
||||
desc={$t('admin.notification_email_host_description')}
|
||||
disabled={disabled || !config.notifications.smtp.enabled}
|
||||
bind:value={config.notifications.smtp.transport.host}
|
||||
isEdited={config.notifications.smtp.transport.host !== savedConfig.notifications.smtp.transport.host}
|
||||
|
|
@ -47,8 +48,8 @@
|
|||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
required
|
||||
label="Port"
|
||||
desc="Port of the email server (e.g 25, 465, or 587)"
|
||||
label={$t('port')}
|
||||
desc={$t('admin.notification_email_port_description')}
|
||||
disabled={disabled || !config.notifications.smtp.enabled}
|
||||
bind:value={config.notifications.smtp.transport.port}
|
||||
isEdited={config.notifications.smtp.transport.port !== savedConfig.notifications.smtp.transport.port}
|
||||
|
|
@ -56,8 +57,8 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="Username"
|
||||
desc="Username to use when authenticating with the email server"
|
||||
label={$t('username')}
|
||||
desc={$t('admin.notification_email_username_description')}
|
||||
disabled={disabled || !config.notifications.smtp.enabled}
|
||||
bind:value={config.notifications.smtp.transport.username}
|
||||
isEdited={config.notifications.smtp.transport.username !==
|
||||
|
|
@ -66,8 +67,8 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.PASSWORD}
|
||||
label="Password"
|
||||
desc="Password to use when authenticating with the email server"
|
||||
label={$t('password')}
|
||||
desc={$t('admin.notification_email_password_description')}
|
||||
disabled={disabled || !config.notifications.smtp.enabled}
|
||||
bind:value={config.notifications.smtp.transport.password}
|
||||
isEdited={config.notifications.smtp.transport.password !==
|
||||
|
|
@ -75,8 +76,8 @@
|
|||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="Ignore certificate errors"
|
||||
subtitle="Ignore TLS certificate validation errors (not recommended)"
|
||||
title={$t('admin.notification_email_ignore_certificate_errors')}
|
||||
subtitle={$t('admin.notification_email_ignore_certificate_errors_description')}
|
||||
disabled={disabled || !config.notifications.smtp.enabled}
|
||||
bind:checked={config.notifications.smtp.transport.ignoreCert}
|
||||
/>
|
||||
|
|
@ -86,8 +87,8 @@
|
|||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
required
|
||||
label="From address"
|
||||
desc="Sender email address, for example: "Immich Photo Server <noreply@immich.app>""
|
||||
label={$t('admin.notification_email_from_address')}
|
||||
desc={$t('admin.notification_email_from_address_description')}
|
||||
disabled={disabled || !config.notifications.smtp.enabled}
|
||||
bind:value={config.notifications.smtp.from}
|
||||
isEdited={config.notifications.smtp.from !== savedConfig.notifications.smtp.from}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
SettingInputFieldType,
|
||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -23,16 +24,16 @@
|
|||
<div class="mt-4 ml-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="EXTERNAL DOMAIN"
|
||||
desc="Domain for public shared links, including http(s)://"
|
||||
label={$t('admin.server_external_domain_settings').toUpperCase()}
|
||||
desc={$t('admin.server_external_domain_settings_description')}
|
||||
bind:value={config.server.externalDomain}
|
||||
isEdited={config.server.externalDomain !== savedConfig.server.externalDomain}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="WELCOME MESSAGE"
|
||||
desc="A message that is displayed on the login page."
|
||||
label={$t('admin.server_welcome_message').toUpperCase()}
|
||||
desc={$t('admin.server_welcome_message_description')}
|
||||
bind:value={config.server.loginPageMessage}
|
||||
isEdited={config.server.loginPageMessage !== savedConfig.server.loginPageMessage}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
SettingInputFieldType,
|
||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -54,10 +55,10 @@
|
|||
const substitutions: Record<string, string> = {
|
||||
filename: 'IMAGE_56437',
|
||||
ext: 'jpg',
|
||||
filetype: 'IMG',
|
||||
filetype: $t('img').toUpperCase(),
|
||||
filetypefull: 'IMAGE',
|
||||
assetId: 'a8312960-e277-447d-b4ea-56717ccba856',
|
||||
album: 'Album Name',
|
||||
album: $t('album_name'),
|
||||
};
|
||||
|
||||
const dt = luxon.DateTime.fromISO(new Date('2022-02-03T04:56:05.250').toISOString());
|
||||
|
|
@ -107,18 +108,18 @@
|
|||
{#await getTemplateOptions() then}
|
||||
<div id="directory-path-builder" class="flex flex-col gap-4 {minified ? '' : 'ml-4 mt-4'}">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
title={$t('enabled').toUpperCase()}
|
||||
{disabled}
|
||||
subtitle="Enable storage template engine"
|
||||
subtitle={$t('admin.storage_template_enable_description')}
|
||||
bind:checked={config.storageTemplate.enabled}
|
||||
isEdited={!(config.storageTemplate.enabled === savedConfig.storageTemplate.enabled)}
|
||||
/>
|
||||
|
||||
{#if !minified}
|
||||
<SettingSwitch
|
||||
title="HASH VERIFICATION ENABLED"
|
||||
title={$t('admin.storage_template_hash_verification_enabled').toUpperCase()}
|
||||
{disabled}
|
||||
subtitle="Enables hash verification, don't disable this unless you're certain of the implications"
|
||||
subtitle={$t('admin.storage_template_hash_verification_enabled_description')}
|
||||
bind:checked={config.storageTemplate.hashVerificationEnabled}
|
||||
isEdited={!(
|
||||
config.storageTemplate.hashVerificationEnabled === savedConfig.storageTemplate.hashVerificationEnabled
|
||||
|
|
@ -129,7 +130,7 @@
|
|||
{#if config.storageTemplate.enabled}
|
||||
<hr />
|
||||
|
||||
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">Variables</h3>
|
||||
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">{$t('variables')}</h3>
|
||||
|
||||
<section class="support-date">
|
||||
{#await getSupportDateTimeFormat()}
|
||||
|
|
@ -146,10 +147,10 @@
|
|||
</section>
|
||||
|
||||
<div class="flex flex-col mt-4">
|
||||
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">Template</h3>
|
||||
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">{$t('template')}</h3>
|
||||
|
||||
<div class="my-2 text-sm">
|
||||
<h4>PREVIEW</h4>
|
||||
<h4>{$t('preview').toUpperCase()}</h4>
|
||||
</div>
|
||||
|
||||
<p class="text-sm">
|
||||
|
|
@ -172,7 +173,7 @@
|
|||
|
||||
<form autocomplete="off" class="flex flex-col" on:submit|preventDefault>
|
||||
<div class="flex flex-col my-2">
|
||||
<label class="text-sm" for="preset-select">PRESET</label>
|
||||
<label class="text-sm" for="preset-select">{$t('preset').toUpperCase()}</label>
|
||||
<select
|
||||
class="immich-form-input p-2 mt-2 text-sm rounded-lg bg-slate-200 hover:cursor-pointer dark:bg-gray-600"
|
||||
disabled={disabled || !config.storageTemplate.enabled}
|
||||
|
|
@ -188,7 +189,7 @@
|
|||
</div>
|
||||
<div class="flex gap-2 align-bottom">
|
||||
<SettingInputField
|
||||
label="TEMPLATE"
|
||||
label={$t('template').toUpperCase()}
|
||||
disabled={disabled || !config.storageTemplate.enabled}
|
||||
required
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
|
|
@ -197,19 +198,24 @@
|
|||
/>
|
||||
|
||||
<div class="flex-0">
|
||||
<SettingInputField label="EXTENSION" inputType={SettingInputFieldType.TEXT} value={'.jpg'} disabled />
|
||||
<SettingInputField
|
||||
label={$t('extension')}
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
value={'.jpg'}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if !minified}
|
||||
<div id="migration-info" class="mt-2 text-sm">
|
||||
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">Notes</h3>
|
||||
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">{$t('notes')}</h3>
|
||||
<section class="flex flex-col gap-2">
|
||||
<p>
|
||||
Template changes will only apply to new assets. To retroactively apply the template to previously
|
||||
uploaded assets, run the
|
||||
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary"
|
||||
>Storage Migration Job</a
|
||||
>{$t('admin.storage_template_migration_job')}</a
|
||||
>.
|
||||
</p>
|
||||
<p>
|
||||
|
|
@ -217,7 +223,7 @@
|
|||
assets, so manually running the
|
||||
|
||||
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary"
|
||||
>Storage Migration Job</a
|
||||
>{$t('admin.storage_template_migration_job')}</a
|
||||
>
|
||||
is required in order to successfully use the variable.
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { locale } from '$lib/stores/preferences.store';
|
||||
import type { SystemConfigTemplateStorageOptionDto } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let options: SystemConfigTemplateStorageOptionDto;
|
||||
|
||||
|
|
@ -21,7 +22,7 @@
|
|||
</div>
|
||||
<div class="flex gap-[40px]">
|
||||
<div>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">YEAR</p>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('year').toUpperCase()}</p>
|
||||
<ul>
|
||||
{#each options.yearOptions as yearFormat}
|
||||
<li>{'{{'}{yearFormat}{'}}'} - {getLuxonExample(yearFormat)}</li>
|
||||
|
|
@ -30,7 +31,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">MONTH</p>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('month').toUpperCase()}</p>
|
||||
<ul>
|
||||
{#each options.monthOptions as monthFormat}
|
||||
<li>{'{{'}{monthFormat}{'}}'} - {getLuxonExample(monthFormat)}</li>
|
||||
|
|
@ -39,7 +40,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">WEEK</p>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('week').toUpperCase()}</p>
|
||||
<ul>
|
||||
{#each options.weekOptions as weekFormat}
|
||||
<li>{'{{'}{weekFormat}{'}}'} - {getLuxonExample(weekFormat)}</li>
|
||||
|
|
@ -48,7 +49,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">DAY</p>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('day').toUpperCase()}</p>
|
||||
<ul>
|
||||
{#each options.dayOptions as dayFormat}
|
||||
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
|
||||
|
|
@ -57,7 +58,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">HOUR</p>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('hour').toUpperCase()}</p>
|
||||
<ul>
|
||||
{#each options.hourOptions as dayFormat}
|
||||
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
|
||||
|
|
@ -66,7 +67,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">MINUTE</p>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('minute').toUpperCase()}</p>
|
||||
<ul>
|
||||
{#each options.minuteOptions as dayFormat}
|
||||
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
|
||||
|
|
@ -75,7 +76,7 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">SECOND</p>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('second').toUpperCase()}</p>
|
||||
<ul>
|
||||
{#each options.secondOptions as dayFormat}
|
||||
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { t } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<div class="mt-4 text-sm">
|
||||
<h4>OTHER VARIABLES</h4>
|
||||
<h4>{$t('other_variables').toUpperCase()}</h4>
|
||||
</div>
|
||||
|
||||
<div class="p-4 mt-2 text-xs bg-gray-200 rounded-lg dark:bg-gray-700 dark:text-immich-dark-fg">
|
||||
<div class="flex gap-[50px]">
|
||||
<div>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">FILENAME</p>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('filename').toUpperCase()}</p>
|
||||
<ul>
|
||||
<li>{`{{filename}}`} - IMG_123</li>
|
||||
<li>{`{{ext}}`} - jpg</li>
|
||||
|
|
@ -13,14 +17,14 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">FILETYPE</p>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('filetype').toUpperCase()}</p>
|
||||
<ul>
|
||||
<li>{`{{filetype}}`} - VID or IMG</li>
|
||||
<li>{`{{filetypefull}}`} - VIDEO or IMAGE</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary uppercase">OTHER</p>
|
||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary uppercase">{$t('other').toUpperCase()}</p>
|
||||
<ul>
|
||||
<li>{`{{assetId}}`} - Asset ID</li>
|
||||
<li>{`{{album}}`} - Album Name</li>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
import type { SettingsEventType } from '../admin-settings';
|
||||
import SettingTextarea from '$lib/components/shared-components/settings/setting-textarea.svelte';
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -21,8 +22,8 @@
|
|||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingTextarea
|
||||
{disabled}
|
||||
label="Custom CSS"
|
||||
desc="Cascading Style Sheets allow the design of Immich to be customized."
|
||||
label={$t('admin.theme_custom_css_settings')}
|
||||
desc={$t('admin.theme_custom_css_settings_description')}
|
||||
bind:value={config.theme.customCss}
|
||||
required={true}
|
||||
isEdited={config.theme.customCss !== savedConfig.theme.customCss}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
SettingInputFieldType,
|
||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -23,9 +24,9 @@
|
|||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
title={$t('enabled').toUpperCase()}
|
||||
{disabled}
|
||||
subtitle="Enable Trash features"
|
||||
subtitle={$t('admin.trash_enabled_description')}
|
||||
bind:checked={config.trash.enabled}
|
||||
/>
|
||||
|
||||
|
|
@ -33,8 +34,8 @@
|
|||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="Number of days"
|
||||
desc="Number of days to keep the assets in trash before permanently removing them"
|
||||
label={$t('admin.trash_number_of_days')}
|
||||
desc={$t('admin.trash_number_of_days_description')}
|
||||
bind:value={config.trash.days}
|
||||
required={true}
|
||||
disabled={disabled || !config.trash.enabled}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
import SettingInputField, {
|
||||
SettingInputFieldType,
|
||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
|
|
@ -25,8 +26,8 @@
|
|||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
min={1}
|
||||
label="DELETE DELAY"
|
||||
desc="Number of days after removal to permanently delete a user's account and assets. The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution."
|
||||
label={$t('admin.user_delete_delay_settings').toUpperCase()}
|
||||
desc={$t('admin.user_delete_delay_settings_description')}
|
||||
bind:value={config.user.deleteDelay}
|
||||
isEdited={config.user.deleteDelay !== savedConfig.user.deleteDelay}
|
||||
/>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue