2022-12-16 14:26:12 -06:00
|
|
|
<script lang="ts">
|
2025-09-19 12:29:01 -04:00
|
|
|
import { resolve } from '$app/paths';
|
2025-09-16 17:15:57 -04:00
|
|
|
import SupportedDatetimePanel from '$lib/components/admin-settings/SupportedDatetimePanel.svelte';
|
|
|
|
|
import SupportedVariablesPanel from '$lib/components/admin-settings/SupportedVariablesPanel.svelte';
|
2025-09-16 15:01:23 -04:00
|
|
|
import SettingButtonsRow from '$lib/components/shared-components/settings/setting-buttons-row.svelte';
|
|
|
|
|
import SettingInputField from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
|
|
|
|
import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte';
|
2024-11-14 08:43:25 -06:00
|
|
|
import { AppRoute, SettingInputFieldType } from '$lib/constants';
|
2025-09-16 15:01:23 -04:00
|
|
|
import FormatMessage from '$lib/elements/FormatMessage.svelte';
|
2024-01-20 13:47:41 -05:00
|
|
|
import { user } from '$lib/stores/user.store';
|
2024-02-13 17:07:37 -05:00
|
|
|
import {
|
|
|
|
|
getStorageTemplateOptions,
|
|
|
|
|
type SystemConfigDto,
|
|
|
|
|
type SystemConfigTemplateStorageOptionDto,
|
|
|
|
|
} from '@immich/sdk';
|
2025-09-16 16:22:13 -04:00
|
|
|
import { LoadingSpinner } from '@immich/ui';
|
2024-01-20 13:47:41 -05:00
|
|
|
import handlebar from 'handlebars';
|
|
|
|
|
import { isEqual } from 'lodash-es';
|
|
|
|
|
import * as luxon from 'luxon';
|
2025-09-16 15:01:23 -04:00
|
|
|
import type { Snippet } from 'svelte';
|
|
|
|
|
import { t } from 'svelte-i18n';
|
|
|
|
|
import { createBubbler, preventDefault } from 'svelte/legacy';
|
2023-07-01 00:50:47 -04:00
|
|
|
import { fade } from 'svelte/transition';
|
2025-09-16 17:15:57 -04:00
|
|
|
import type { SettingsResetEvent, SettingsSaveEvent } from './admin-settings';
|
2024-11-14 08:43:25 -06:00
|
|
|
|
|
|
|
|
interface Props {
|
|
|
|
|
savedConfig: SystemConfigDto;
|
|
|
|
|
defaultConfig: SystemConfigDto;
|
|
|
|
|
config: SystemConfigDto;
|
|
|
|
|
disabled?: boolean;
|
|
|
|
|
minified?: boolean;
|
|
|
|
|
onReset: SettingsResetEvent;
|
|
|
|
|
onSave: SettingsSaveEvent;
|
|
|
|
|
duration?: number;
|
|
|
|
|
children?: Snippet;
|
|
|
|
|
}
|
2023-07-01 00:50:47 -04:00
|
|
|
|
2024-11-14 08:43:25 -06:00
|
|
|
let {
|
|
|
|
|
savedConfig,
|
|
|
|
|
defaultConfig,
|
|
|
|
|
config = $bindable(),
|
|
|
|
|
disabled = false,
|
|
|
|
|
minified = false,
|
|
|
|
|
onReset,
|
|
|
|
|
onSave,
|
|
|
|
|
duration = 500,
|
|
|
|
|
children,
|
|
|
|
|
}: Props = $props();
|
2023-07-01 00:50:47 -04:00
|
|
|
|
2025-09-16 15:01:23 -04:00
|
|
|
const bubble = createBubbler();
|
2024-11-14 08:43:25 -06:00
|
|
|
let templateOptions: SystemConfigTemplateStorageOptionDto | undefined = $state();
|
|
|
|
|
let selectedPreset = $state('');
|
2023-07-01 00:50:47 -04:00
|
|
|
|
2024-01-12 18:44:11 +01:00
|
|
|
const getTemplateOptions = async () => {
|
2024-02-13 17:07:37 -05:00
|
|
|
templateOptions = await getStorageTemplateOptions();
|
2024-01-12 18:44:11 +01:00
|
|
|
selectedPreset = savedConfig.storageTemplate.template;
|
2023-12-14 17:55:15 +01:00
|
|
|
};
|
|
|
|
|
|
2024-02-13 17:07:37 -05:00
|
|
|
const getSupportDateTimeFormat = () => getStorageTemplateOptions();
|
2023-07-01 00:50:47 -04:00
|
|
|
|
2024-11-14 08:43:25 -06:00
|
|
|
const renderTemplate = (templateString: string) => {
|
|
|
|
|
if (!templateOptions) {
|
|
|
|
|
return '';
|
2023-07-01 00:50:47 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const template = handlebar.compile(templateString, {
|
|
|
|
|
knownHelpers: undefined,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const substitutions: Record<string, string> = {
|
|
|
|
|
filename: 'IMAGE_56437',
|
|
|
|
|
ext: 'jpg',
|
2024-06-12 13:13:10 +02:00
|
|
|
filetype: 'IMG',
|
2023-07-01 00:50:47 -04:00
|
|
|
filetypefull: 'IMAGE',
|
2023-10-20 14:17:17 -07:00
|
|
|
assetId: 'a8312960-e277-447d-b4ea-56717ccba856',
|
2025-02-28 16:04:34 -05:00
|
|
|
assetIdShort: '56717ccba856',
|
2024-06-04 21:53:00 +02:00
|
|
|
album: $t('album_name'),
|
2023-07-01 00:50:47 -04:00
|
|
|
};
|
|
|
|
|
|
2023-09-28 19:47:31 +02:00
|
|
|
const dt = luxon.DateTime.fromISO(new Date('2022-02-03T04:56:05.250').toISOString());
|
2025-04-21 16:54:33 -07:00
|
|
|
const albumStartTime = luxon.DateTime.fromISO(new Date('2021-12-31T05:32:41.750').toISOString());
|
|
|
|
|
const albumEndTime = luxon.DateTime.fromISO(new Date('2023-05-06T09:15:17.100').toISOString());
|
2023-07-01 00:50:47 -04:00
|
|
|
|
|
|
|
|
const dateTokens = [
|
|
|
|
|
...templateOptions.yearOptions,
|
|
|
|
|
...templateOptions.monthOptions,
|
2023-09-28 19:47:31 +02:00
|
|
|
...templateOptions.weekOptions,
|
2023-07-01 00:50:47 -04:00
|
|
|
...templateOptions.dayOptions,
|
|
|
|
|
...templateOptions.hourOptions,
|
|
|
|
|
...templateOptions.minuteOptions,
|
|
|
|
|
...templateOptions.secondOptions,
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const token of dateTokens) {
|
|
|
|
|
substitutions[token] = dt.toFormat(token);
|
2025-04-21 16:54:33 -07:00
|
|
|
substitutions['album-startDate-' + token] = albumStartTime.toFormat(token);
|
|
|
|
|
substitutions['album-endDate-' + token] = albumEndTime.toFormat(token);
|
2023-07-01 00:50:47 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return template(substitutions);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handlePresetSelection = () => {
|
2024-01-12 18:44:11 +01:00
|
|
|
config.storageTemplate.template = selectedPreset;
|
2023-07-01 00:50:47 -04:00
|
|
|
};
|
2024-11-14 08:43:25 -06:00
|
|
|
let parsedTemplate = $derived(() => {
|
|
|
|
|
try {
|
|
|
|
|
return renderTemplate(config.storageTemplate.template);
|
|
|
|
|
} catch {
|
|
|
|
|
return 'error';
|
|
|
|
|
}
|
|
|
|
|
});
|
2022-12-16 14:26:12 -06:00
|
|
|
</script>
|
|
|
|
|
|
2024-02-23 22:48:19 +05:30
|
|
|
<section class="dark:text-immich-dark-fg mt-2">
|
2024-08-13 19:01:30 +02:00
|
|
|
<div in:fade={{ duration }} class="mx-4 flex flex-col gap-4 py-4">
|
2024-02-23 22:48:19 +05:30
|
|
|
<p class="text-sm dark:text-immich-dark-fg">
|
2024-11-14 08:43:25 -06:00
|
|
|
<FormatMessage key="admin.storage_template_more_details">
|
|
|
|
|
{#snippet children({ tag, message })}
|
|
|
|
|
{#if tag === 'template-link'}
|
|
|
|
|
<a
|
2025-10-02 17:42:14 +02:00
|
|
|
href="https://docs.immich.app/administration/storage-template"
|
2024-11-14 08:43:25 -06:00
|
|
|
class="underline"
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
>
|
|
|
|
|
{message}
|
|
|
|
|
</a>
|
|
|
|
|
{:else if tag === 'implications-link'}
|
|
|
|
|
<a
|
2025-10-02 17:42:14 +02:00
|
|
|
href="https://docs.immich.app/administration/backup-and-restore#asset-types-and-storage-locations"
|
2024-11-14 08:43:25 -06:00
|
|
|
class="underline"
|
|
|
|
|
target="_blank"
|
|
|
|
|
rel="noreferrer"
|
|
|
|
|
>
|
|
|
|
|
{message}
|
|
|
|
|
</a>
|
|
|
|
|
{/if}
|
|
|
|
|
{/snippet}
|
2024-06-21 22:08:36 +02:00
|
|
|
</FormatMessage>
|
2024-02-23 22:48:19 +05:30
|
|
|
</p>
|
|
|
|
|
</div>
|
2024-01-12 18:44:11 +01:00
|
|
|
{#await getTemplateOptions() then}
|
2025-04-28 09:53:53 -04:00
|
|
|
<div id="directory-path-builder" class="flex flex-col gap-4 {minified ? '' : 'ms-4 mt-4'}">
|
2023-12-29 18:41:33 +00:00
|
|
|
<SettingSwitch
|
2024-06-14 07:32:41 +00:00
|
|
|
title={$t('admin.storage_template_enable_description')}
|
2023-12-29 18:41:33 +00:00
|
|
|
{disabled}
|
2024-01-12 18:44:11 +01:00
|
|
|
bind:checked={config.storageTemplate.enabled}
|
|
|
|
|
isEdited={!(config.storageTemplate.enabled === savedConfig.storageTemplate.enabled)}
|
2023-12-29 18:41:33 +00:00
|
|
|
/>
|
|
|
|
|
|
2024-01-03 23:28:32 -06:00
|
|
|
{#if !minified}
|
|
|
|
|
<SettingSwitch
|
2024-06-14 07:32:41 +00:00
|
|
|
title={$t('admin.storage_template_hash_verification_enabled')}
|
2024-01-03 23:28:32 -06:00
|
|
|
{disabled}
|
2024-06-04 21:53:00 +02:00
|
|
|
subtitle={$t('admin.storage_template_hash_verification_enabled_description')}
|
2024-01-12 18:44:11 +01:00
|
|
|
bind:checked={config.storageTemplate.hashVerificationEnabled}
|
|
|
|
|
isEdited={!(
|
|
|
|
|
config.storageTemplate.hashVerificationEnabled === savedConfig.storageTemplate.hashVerificationEnabled
|
|
|
|
|
)}
|
2024-01-03 23:28:32 -06:00
|
|
|
/>
|
|
|
|
|
{/if}
|
|
|
|
|
|
2024-01-12 18:44:11 +01:00
|
|
|
{#if config.storageTemplate.enabled}
|
2024-01-03 23:28:32 -06:00
|
|
|
<hr />
|
|
|
|
|
|
2025-09-17 12:12:51 -04:00
|
|
|
<h3 class="text-base font-medium text-primary">{$t('variables')}</h3>
|
2024-01-03 23:28:32 -06:00
|
|
|
|
|
|
|
|
<section class="support-date">
|
|
|
|
|
{#await getSupportDateTimeFormat()}
|
|
|
|
|
<LoadingSpinner />
|
|
|
|
|
{:then options}
|
|
|
|
|
<div transition:fade={{ duration: 200 }}>
|
|
|
|
|
<SupportedDatetimePanel {options} />
|
|
|
|
|
</div>
|
|
|
|
|
{/await}
|
|
|
|
|
</section>
|
2023-12-29 18:41:33 +00:00
|
|
|
|
2024-01-03 23:28:32 -06:00
|
|
|
<section class="support-date">
|
|
|
|
|
<SupportedVariablesPanel />
|
|
|
|
|
</section>
|
2023-12-29 18:41:33 +00:00
|
|
|
|
2024-01-03 23:28:32 -06:00
|
|
|
<div class="flex flex-col mt-4">
|
2025-09-17 12:12:51 -04:00
|
|
|
<h3 class="text-base font-medium text-primary">{$t('template')}</h3>
|
2023-07-01 00:50:47 -04:00
|
|
|
|
2024-01-03 23:28:32 -06:00
|
|
|
<div class="my-2 text-sm">
|
2025-09-16 00:28:42 -03:00
|
|
|
<h4 class="uppercase">{$t('preview')}</h4>
|
2023-07-01 00:50:47 -04:00
|
|
|
</div>
|
|
|
|
|
|
2024-01-03 23:28:32 -06:00
|
|
|
<p class="text-sm">
|
2024-06-21 22:08:36 +02:00
|
|
|
<FormatMessage
|
2024-06-22 18:08:56 +02:00
|
|
|
key="admin.storage_template_path_length"
|
2024-06-21 22:08:36 +02:00
|
|
|
values={{ length: parsedTemplate().length + $user.id.length + 'UPLOAD_LOCATION'.length, limit: 260 }}
|
|
|
|
|
>
|
2024-11-14 08:43:25 -06:00
|
|
|
{#snippet children({ message })}
|
2025-09-17 12:12:51 -04:00
|
|
|
<span class="font-semibold text-primary">{message}</span>
|
2024-11-14 08:43:25 -06:00
|
|
|
{/snippet}
|
2024-06-21 22:08:36 +02:00
|
|
|
</FormatMessage>
|
2024-01-03 23:28:32 -06:00
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<p class="text-sm">
|
2024-11-14 08:43:25 -06:00
|
|
|
<FormatMessage key="admin.storage_template_user_label" values={{ label: $user.storageLabel || $user.id }}>
|
|
|
|
|
{#snippet children({ message })}
|
2025-09-17 12:12:51 -04:00
|
|
|
<code class="text-primary">{message}</code>
|
2024-11-14 08:43:25 -06:00
|
|
|
{/snippet}
|
2024-06-21 22:08:36 +02:00
|
|
|
</FormatMessage>
|
2024-01-03 23:28:32 -06:00
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
<p class="p-4 py-2 mt-2 text-xs bg-gray-200 rounded-lg dark:bg-gray-700 dark:text-immich-dark-fg">
|
|
|
|
|
<span class="text-immich-fg/25 dark:text-immich-dark-fg/50"
|
2025-02-27 09:46:20 -06:00
|
|
|
>UPLOAD_LOCATION/library/{$user.storageLabel || $user.id}</span
|
2024-01-03 23:28:32 -06:00
|
|
|
>/{parsedTemplate()}.jpg
|
|
|
|
|
</p>
|
|
|
|
|
|
2024-11-14 08:43:25 -06:00
|
|
|
<form autocomplete="off" class="flex flex-col" onsubmit={preventDefault(bubble('submit'))}>
|
2024-01-03 23:28:32 -06:00
|
|
|
<div class="flex flex-col my-2">
|
2024-11-14 08:43:25 -06:00
|
|
|
{#if templateOptions}
|
2025-09-17 12:12:51 -04:00
|
|
|
<label class="font-medium text-primary text-sm" for="preset-select">
|
2024-11-14 08:43:25 -06:00
|
|
|
{$t('preset')}
|
|
|
|
|
</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}
|
|
|
|
|
name="presets"
|
|
|
|
|
id="preset-select"
|
|
|
|
|
bind:value={selectedPreset}
|
|
|
|
|
onchange={handlePresetSelection}
|
|
|
|
|
>
|
2025-03-03 14:24:26 +00:00
|
|
|
{#each templateOptions.presetOptions as preset (preset)}
|
2024-11-14 08:43:25 -06:00
|
|
|
<option value={preset}>{renderTemplate(preset)}</option>
|
|
|
|
|
{/each}
|
|
|
|
|
</select>
|
|
|
|
|
{/if}
|
2024-01-03 23:28:32 -06:00
|
|
|
</div>
|
2024-11-14 08:43:25 -06:00
|
|
|
|
2024-01-03 23:28:32 -06:00
|
|
|
<div class="flex gap-2 align-bottom">
|
|
|
|
|
<SettingInputField
|
2024-06-14 07:32:41 +00:00
|
|
|
label={$t('template')}
|
2024-01-12 18:44:11 +01:00
|
|
|
disabled={disabled || !config.storageTemplate.enabled}
|
2024-01-03 23:28:32 -06:00
|
|
|
required
|
|
|
|
|
inputType={SettingInputFieldType.TEXT}
|
2024-01-12 18:44:11 +01:00
|
|
|
bind:value={config.storageTemplate.template}
|
|
|
|
|
isEdited={!(config.storageTemplate.template === savedConfig.storageTemplate.template)}
|
2024-01-03 23:28:32 -06:00
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<div class="flex-0">
|
2024-06-04 21:53:00 +02:00
|
|
|
<SettingInputField
|
|
|
|
|
label={$t('extension')}
|
|
|
|
|
inputType={SettingInputFieldType.TEXT}
|
2025-03-03 14:24:26 +00:00
|
|
|
value=".jpg"
|
2024-06-04 21:53:00 +02:00
|
|
|
disabled
|
|
|
|
|
/>
|
2024-01-03 23:28:32 -06:00
|
|
|
</div>
|
2023-07-01 00:50:47 -04:00
|
|
|
</div>
|
|
|
|
|
|
2024-01-03 23:28:32 -06:00
|
|
|
{#if !minified}
|
|
|
|
|
<div id="migration-info" class="mt-2 text-sm">
|
2025-09-17 12:12:51 -04:00
|
|
|
<h3 class="text-base font-medium text-primary">{$t('notes')}</h3>
|
2024-01-03 23:28:32 -06:00
|
|
|
<section class="flex flex-col gap-2">
|
|
|
|
|
<p>
|
2024-06-21 22:08:36 +02:00
|
|
|
<FormatMessage
|
2024-06-22 18:08:56 +02:00
|
|
|
key="admin.storage_template_migration_info"
|
2024-06-21 22:08:36 +02:00
|
|
|
values={{ job: $t('admin.storage_template_migration_job') }}
|
2024-01-03 23:28:32 -06:00
|
|
|
>
|
2024-11-14 08:43:25 -06:00
|
|
|
{#snippet children({ message })}
|
2025-09-19 12:29:01 -04:00
|
|
|
<a href={resolve(AppRoute.ADMIN_JOBS)} class="text-primary">
|
2024-11-14 08:43:25 -06:00
|
|
|
{message}
|
|
|
|
|
</a>
|
|
|
|
|
{/snippet}
|
2024-06-21 22:08:36 +02:00
|
|
|
</FormatMessage>
|
2024-01-03 23:28:32 -06:00
|
|
|
</p>
|
|
|
|
|
</section>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
</form>
|
|
|
|
|
</div>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
{#if minified}
|
2024-11-14 08:43:25 -06:00
|
|
|
{@render children?.()}
|
2024-01-03 23:28:32 -06:00
|
|
|
{:else}
|
|
|
|
|
<SettingButtonsRow
|
2024-07-10 15:57:18 +02:00
|
|
|
onReset={(options) => onReset({ ...options, configKeys: ['storageTemplate'] })}
|
|
|
|
|
onSave={() => onSave({ storageTemplate: config.storageTemplate })}
|
2024-04-07 12:43:40 -04:00
|
|
|
showResetToDefault={!isEqual(savedConfig.storageTemplate, defaultConfig.storageTemplate) && !minified}
|
2024-01-03 23:28:32 -06:00
|
|
|
{disabled}
|
|
|
|
|
/>
|
|
|
|
|
{/if}
|
2023-07-01 00:50:47 -04:00
|
|
|
</div>
|
|
|
|
|
{/await}
|
2022-12-16 14:26:12 -06:00
|
|
|
</section>
|