1
0
mirror of https://github.com/immich-app/immich.git synced 2025-06-19 04:07:43 +02:00

feat(server) user-defined storage structure (#1098)

[Breaking] newly uploaded file will conform to the default structure of `{uploadLocation}/{userId}/year/year-month-day/filename.ext`
This commit is contained in:
Alex
2022-12-16 14:26:12 -06:00
committed by GitHub
parent 391d00bcb9
commit c754c860fd
59 changed files with 1892 additions and 173 deletions

View File

@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.38.0
* The version of the OpenAPI document: 1.38.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
@ -1443,6 +1443,12 @@ export interface SystemConfigDto {
* @memberof SystemConfigDto
*/
'oauth': SystemConfigOAuthDto;
/**
*
* @type {SystemConfigStorageTemplateDto}
* @memberof SystemConfigDto
*/
'storageTemplate': SystemConfigStorageTemplateDto;
}
/**
*
@ -1530,6 +1536,68 @@ export interface SystemConfigOAuthDto {
*/
'autoRegister': boolean;
}
/**
*
* @export
* @interface SystemConfigStorageTemplateDto
*/
export interface SystemConfigStorageTemplateDto {
/**
*
* @type {string}
* @memberof SystemConfigStorageTemplateDto
*/
'template': string;
}
/**
*
* @export
* @interface SystemConfigTemplateStorageOptionDto
*/
export interface SystemConfigTemplateStorageOptionDto {
/**
*
* @type {Array<string>}
* @memberof SystemConfigTemplateStorageOptionDto
*/
'yearOptions': Array<string>;
/**
*
* @type {Array<string>}
* @memberof SystemConfigTemplateStorageOptionDto
*/
'monthOptions': Array<string>;
/**
*
* @type {Array<string>}
* @memberof SystemConfigTemplateStorageOptionDto
*/
'dayOptions': Array<string>;
/**
*
* @type {Array<string>}
* @memberof SystemConfigTemplateStorageOptionDto
*/
'hourOptions': Array<string>;
/**
*
* @type {Array<string>}
* @memberof SystemConfigTemplateStorageOptionDto
*/
'minuteOptions': Array<string>;
/**
*
* @type {Array<string>}
* @memberof SystemConfigTemplateStorageOptionDto
*/
'secondOptions': Array<string>;
/**
*
* @type {Array<string>}
* @memberof SystemConfigTemplateStorageOptionDto
*/
'presetOptions': Array<string>;
}
/**
*
* @export
@ -5312,6 +5380,39 @@ export const SystemConfigApiAxiosParamCreator = function (configuration?: Config
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
return {
url: toPathString(localVarUrlObj),
options: localVarRequestOptions,
};
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getStorageTemplateOptions: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
const localVarPath = `/system-config/storage-template-options`;
// use dummy base URL string because the URL constructor only accepts absolute URLs.
const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL);
let baseOptions;
if (configuration) {
baseOptions = configuration.baseOptions;
}
const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options};
const localVarHeaderParameter = {} as any;
const localVarQueryParameter = {} as any;
// authentication bearer required
// http bearer authentication required
await setBearerAuthToObject(localVarHeaderParameter, configuration)
setSearchParams(localVarUrlObj, localVarQueryParameter);
let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
@ -5388,6 +5489,15 @@ export const SystemConfigApiFp = function(configuration?: Configuration) {
const localVarAxiosArgs = await localVarAxiosParamCreator.getDefaults(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
async getStorageTemplateOptions(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise<SystemConfigTemplateStorageOptionDto>> {
const localVarAxiosArgs = await localVarAxiosParamCreator.getStorageTemplateOptions(options);
return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration);
},
/**
*
* @param {SystemConfigDto} systemConfigDto
@ -5424,6 +5534,14 @@ export const SystemConfigApiFactory = function (configuration?: Configuration, b
getDefaults(options?: any): AxiosPromise<SystemConfigDto> {
return localVarFp.getDefaults(options).then((request) => request(axios, basePath));
},
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
*/
getStorageTemplateOptions(options?: any): AxiosPromise<SystemConfigTemplateStorageOptionDto> {
return localVarFp.getStorageTemplateOptions(options).then((request) => request(axios, basePath));
},
/**
*
* @param {SystemConfigDto} systemConfigDto
@ -5463,6 +5581,16 @@ export class SystemConfigApi extends BaseAPI {
return SystemConfigApiFp(this.configuration).getDefaults(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {*} [options] Override http request option.
* @throws {RequiredError}
* @memberof SystemConfigApi
*/
public getStorageTemplateOptions(options?: AxiosRequestConfig) {
return SystemConfigApiFp(this.configuration).getStorageTemplateOptions(options).then((request) => request(this.axios, this.basePath));
}
/**
*
* @param {SystemConfigDto} systemConfigDto

View File

@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.38.0
* The version of the OpenAPI document: 1.38.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.38.0
* The version of the OpenAPI document: 1.38.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.38.0
* The version of the OpenAPI document: 1.38.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@ -4,7 +4,7 @@
* Immich
* Immich API
*
* The version of the OpenAPI document: 1.38.0
* The version of the OpenAPI document: 1.38.2
*
*
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).

View File

@ -59,11 +59,11 @@ input:focus-visible {
@layer utilities {
.immich-form-input {
@apply bg-slate-100 p-2 rounded-md focus:border-immich-primary text-sm dark:bg-gray-600 dark:text-immich-dark-fg disabled:bg-gray-500 dark:disabled:bg-gray-900 disabled:cursor-not-allowed;
@apply bg-slate-200 p-2 rounded-lg focus:border-immich-primary text-sm dark:bg-gray-600 dark:text-immich-dark-fg disabled:bg-gray-400 dark:disabled:bg-gray-800 disabled:cursor-not-allowed disabled:text-gray-200;
}
.immich-form-label {
@apply font-medium text-sm text-gray-500 dark:text-gray-300;
@apply font-medium text-gray-500 dark:text-gray-300;
}
.immich-btn-primary {

View File

@ -25,12 +25,12 @@
const { data: configs } = await api.systemConfigApi.getConfig();
const result = await api.systemConfigApi.updateConfig({
ffmpeg: ffmpegConfig,
oauth: configs.oauth
...configs,
ffmpeg: ffmpegConfig
});
ffmpegConfig = result.data.ffmpeg;
savedConfig = result.data.ffmpeg;
ffmpegConfig = { ...result.data.ffmpeg };
savedConfig = { ...result.data.ffmpeg };
notificationController.show({
message: 'FFmpeg settings saved',
@ -48,8 +48,8 @@
async function reset() {
const { data: resetConfig } = await api.systemConfigApi.getConfig();
ffmpegConfig = resetConfig.ffmpeg;
savedConfig = resetConfig.ffmpeg;
ffmpegConfig = { ...resetConfig.ffmpeg };
savedConfig = { ...resetConfig.ffmpeg };
notificationController.show({
message: 'Reset FFmpeg settings to the recent saved settings',
@ -60,8 +60,8 @@
async function resetToDefault() {
const { data: configs } = await api.systemConfigApi.getDefaults();
ffmpegConfig = configs.ffmpeg;
defaultConfig = configs.ffmpeg;
ffmpegConfig = { ...configs.ffmpeg };
defaultConfig = { ...configs.ffmpeg };
notificationController.show({
message: 'Reset FFmpeg settings to default',
@ -74,52 +74,56 @@
{#await getConfigs() then}
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label="CRF"
bind:value={ffmpegConfig.crf}
required={true}
isEdited={!(ffmpegConfig.crf == savedConfig.crf)}
/>
<div class="flex flex-col gap-4 ml-4 mt-4">
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label="CRF"
bind:value={ffmpegConfig.crf}
required={true}
isEdited={!(ffmpegConfig.crf == savedConfig.crf)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="PRESET"
bind:value={ffmpegConfig.preset}
required={true}
isEdited={!(ffmpegConfig.preset == savedConfig.preset)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="PRESET"
bind:value={ffmpegConfig.preset}
required={true}
isEdited={!(ffmpegConfig.preset == savedConfig.preset)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="AUDIO CODEC"
bind:value={ffmpegConfig.targetAudioCodec}
required={true}
isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="AUDIO CODEC"
bind:value={ffmpegConfig.targetAudioCodec}
required={true}
isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="VIDEO CODEC"
bind:value={ffmpegConfig.targetVideoCodec}
required={true}
isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="VIDEO CODEC"
bind:value={ffmpegConfig.targetVideoCodec}
required={true}
isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="SCALING"
bind:value={ffmpegConfig.targetScaling}
required={true}
isEdited={!(ffmpegConfig.targetScaling == savedConfig.targetScaling)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="SCALING"
bind:value={ffmpegConfig.targetScaling}
required={true}
isEdited={!(ffmpegConfig.targetScaling == savedConfig.targetScaling)}
/>
</div>
<SettingButtonsRow
on:reset={reset}
on:save={saveSetting}
on:reset-to-default={resetToDefault}
showResetToDefault={!_.isEqual(savedConfig, defaultConfig)}
/>
<div class="ml-4">
<SettingButtonsRow
on:reset={reset}
on:save={saveSetting}
on:reset-to-default={resetToDefault}
showResetToDefault={!_.isEqual(savedConfig, defaultConfig)}
/>
</div>
</form>
</div>
{/await}

View File

@ -25,8 +25,8 @@
async function reset() {
const { data: resetConfig } = await api.systemConfigApi.getConfig();
oauthConfig = resetConfig.oauth;
savedConfig = resetConfig.oauth;
oauthConfig = { ...resetConfig.oauth };
savedConfig = { ...resetConfig.oauth };
notificationController.show({
message: 'Reset OAuth settings to the last saved settings',
@ -39,12 +39,12 @@
const { data: currentConfig } = await api.systemConfigApi.getConfig();
const result = await api.systemConfigApi.updateConfig({
ffmpeg: currentConfig.ffmpeg,
...currentConfig,
oauth: oauthConfig
});
oauthConfig = result.data.oauth;
savedConfig = result.data.oauth;
oauthConfig = { ...result.data.oauth };
savedConfig = { ...result.data.oauth };
notificationController.show({
message: 'OAuth settings saved',
@ -62,7 +62,7 @@
async function resetToDefault() {
const { data: defaultConfig } = await api.systemConfigApi.getDefaults();
oauthConfig = defaultConfig.oauth;
oauthConfig = { ...defaultConfig.oauth };
notificationController.show({
message: 'Reset OAuth settings to default',
@ -80,51 +80,52 @@
</div>
<hr class="m-4" />
<div class="flex flex-col gap-4 ml-4">
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="ISSUER URL"
bind:value={oauthConfig.issuerUrl}
required={true}
disabled={!oauthConfig.enabled}
isEdited={!(oauthConfig.issuerUrl == savedConfig.issuerUrl)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="ISSUER URL"
bind:value={oauthConfig.issuerUrl}
required={true}
disabled={!oauthConfig.enabled}
isEdited={!(oauthConfig.issuerUrl == savedConfig.issuerUrl)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="CLIENT ID"
bind:value={oauthConfig.clientId}
required={true}
disabled={!oauthConfig.enabled}
isEdited={!(oauthConfig.clientId == savedConfig.clientId)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="CLIENT ID"
bind:value={oauthConfig.clientId}
required={true}
disabled={!oauthConfig.enabled}
isEdited={!(oauthConfig.clientId == savedConfig.clientId)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="CLIENT SECRET"
bind:value={oauthConfig.clientSecret}
required={true}
disabled={!oauthConfig.enabled}
isEdited={!(oauthConfig.clientSecret == savedConfig.clientSecret)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="CLIENT SECRET"
bind:value={oauthConfig.clientSecret}
required={true}
disabled={!oauthConfig.enabled}
isEdited={!(oauthConfig.clientSecret == savedConfig.clientSecret)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="SCOPE"
bind:value={oauthConfig.scope}
required={true}
disabled={!oauthConfig.enabled}
isEdited={!(oauthConfig.scope == savedConfig.scope)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="SCOPE"
bind:value={oauthConfig.scope}
required={true}
disabled={!oauthConfig.enabled}
isEdited={!(oauthConfig.scope == savedConfig.scope)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="BUTTON TEXT"
bind:value={oauthConfig.buttonText}
required={false}
disabled={!oauthConfig.enabled}
isEdited={!(oauthConfig.buttonText == savedConfig.buttonText)}
/>
<SettingInputField
inputType={SettingInputFieldType.TEXT}
label="BUTTON TEXT"
bind:value={oauthConfig.buttonText}
required={false}
disabled={!oauthConfig.enabled}
isEdited={!(oauthConfig.buttonText == savedConfig.buttonText)}
/>
</div>
<div class="mt-4">
<SettingSwitch
@ -135,12 +136,14 @@
/>
</div>
<SettingButtonsRow
on:reset={reset}
on:save={saveSetting}
on:reset-to-default={resetToDefault}
showResetToDefault={!_.isEqual(savedConfig, defaultConfig)}
/>
<div class="ml-4">
<SettingButtonsRow
on:reset={reset}
on:save={saveSetting}
on:reset-to-default={resetToDefault}
showResetToDefault={!_.isEqual(savedConfig, defaultConfig)}
/>
</div>
</form>
</div>
{/await}

View File

@ -6,11 +6,11 @@
export let showResetToDefault = true;
</script>
<div class="flex justify-between gap-2 mx-4 mt-8">
<div class="flex justify-between gap-2 mt-8">
<div class="left">
{#if showResetToDefault}
<button
on:click|preventDefault={() => dispatch('reset-to-default')}
on:click={() => dispatch('reset-to-default')}
class="text-sm dark:text-immich-dark-primary hover:dark:text-immich-dark-primary/75 text-immich-primary hover:text-immich-primary/75 font-medium bg-none"
>
Reset to default
@ -20,7 +20,7 @@
<div class="right">
<button
on:click|preventDefault={() => dispatch('reset')}
on:click={() => dispatch('reset')}
class="text-sm bg-gray-500 dark:bg-gray-200 hover:bg-gray-500/75 dark:hover:bg-gray-200/80 px-4 py-2 text-white dark:text-immich-dark-gray rounded-full shadow-md font-medium disabled:opacity-50 disabled:cursor-not-allowed"
>Reset
</button>

View File

@ -12,19 +12,19 @@
export let inputType: SettingInputFieldType;
export let value: string;
export let label: string;
export let label = '';
export let required = false;
export let disabled = false;
export let isEdited: boolean;
export let isEdited = false;
const handleInput = (e: Event) => {
value = (e.target as HTMLInputElement).value;
};
</script>
<div class="m-4 flex flex-col gap-2">
<div class="flex place-items-center gap-1">
<label class="immich-form-label" for={label}>{label.toUpperCase()} </label>
<div class="w-full">
<div class={`flex place-items-center gap-1 h-[26px]`}>
<label class={`immich-form-label text-xs`} for={label}>{label.toUpperCase()} </label>
{#if required}
<div class="text-red-400">*</div>
{/if}
@ -32,14 +32,14 @@
{#if isEdited}
<div
transition:fly={{ x: 10, duration: 200, easing: quintOut }}
class="text-gray-500 text-xs italic"
class="bg-orange-100 px-2 rounded-full text-orange-900 text-[10px]"
>
Unsaved change
</div>
{/if}
</div>
<input
class="immich-form-input"
class="immich-form-input w-full"
id={label}
name={label}
type={inputType}

View File

@ -7,7 +7,7 @@
<div class="flex justify-between mx-4 place-items-center">
<div>
<h2 class="immich-form-label">
<h2 class="immich-form-label text-sm">
{title.toUpperCase()}
</h2>

View File

@ -0,0 +1,227 @@
<script lang="ts">
import {
api,
SystemConfigStorageTemplateDto,
SystemConfigTemplateStorageOptionDto,
UserResponseDto
} from '@api';
import * as luxon from 'luxon';
import handlebar from 'handlebars';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { fade } from 'svelte/transition';
import SupportedDatetimePanel from './supported-datetime-panel.svelte';
import SupportedVariablesPanel from './supported-variables-panel.svelte';
import SettingButtonsRow from '../setting-buttons-row.svelte';
import _ from 'lodash';
import {
notificationController,
NotificationType
} from '$lib/components/shared-components/notification/notification';
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
export let storageConfig: SystemConfigStorageTemplateDto;
export let user: UserResponseDto;
let savedConfig: SystemConfigStorageTemplateDto;
let defaultConfig: SystemConfigStorageTemplateDto;
let templateOptions: SystemConfigTemplateStorageOptionDto;
let selectedPreset = '';
async function getConfigs() {
[savedConfig, defaultConfig, templateOptions] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.storageTemplate),
api.systemConfigApi.getDefaults().then((res) => res.data.storageTemplate),
api.systemConfigApi.getStorageTemplateOptions().then((res) => res.data)
]);
selectedPreset = templateOptions.presetOptions[0];
}
const getSupportDateTimeFormat = async () => {
const { data } = await api.systemConfigApi.getStorageTemplateOptions();
return data;
};
$: parsedTemplate = () => {
try {
return renderTemplate(storageConfig.template);
} catch (error) {
return 'error';
}
};
const renderTemplate = (templateString: string) => {
const template = handlebar.compile(templateString, {
knownHelpers: undefined
});
const substitutions: Record<string, string> = {
filename: 'IMG_10041123',
ext: 'jpeg'
};
const dt = luxon.DateTime.fromISO(new Date('2022-09-04T20:03:05.250').toISOString());
const dateTokens = [
...templateOptions.yearOptions,
...templateOptions.monthOptions,
...templateOptions.dayOptions,
...templateOptions.hourOptions,
...templateOptions.minuteOptions,
...templateOptions.secondOptions
];
for (const token of dateTokens) {
substitutions[token] = dt.toFormat(token);
}
return template(substitutions);
};
async function reset() {
const { data: resetConfig } = await api.systemConfigApi.getConfig();
storageConfig.template = resetConfig.storageTemplate.template;
savedConfig.template = resetConfig.storageTemplate.template;
notificationController.show({
message: 'Reset storage template settings to the recent saved settings',
type: NotificationType.Info
});
}
async function saveSetting() {
try {
const { data: currentConfig } = await api.systemConfigApi.getConfig();
const result = await api.systemConfigApi.updateConfig({
...currentConfig,
storageTemplate: storageConfig
});
storageConfig.template = result.data.storageTemplate.template;
savedConfig.template = result.data.storageTemplate.template;
notificationController.show({
message: 'Storage template saved',
type: NotificationType.Info
});
} catch (e) {
console.error('Error [storage-template-settings] [saveSetting]', e);
notificationController.show({
message: 'Unable to save settings',
type: NotificationType.Error
});
}
}
async function resetToDefault() {
const { data: defaultConfig } = await api.systemConfigApi.getDefaults();
storageConfig.template = defaultConfig.storageTemplate.template;
notificationController.show({
message: 'Reset storage template to default',
type: NotificationType.Info
});
}
const handlePresetSelection = () => {
storageConfig.template = selectedPreset;
};
</script>
<section class="dark:text-immich-dark-fg">
{#await getConfigs() then}
<div id="directory-path-builder" class="m-4">
<h3 class="font-medium text-immich-primary dark:text-immich-dark-primary text-base">
Variables
</h3>
<section class="support-date">
{#await getSupportDateTimeFormat()}
<LoadingSpinner />
{:then options}
<div transition:fade={{ duration: 200 }}>
<SupportedDatetimePanel {options} />
</div>
{/await}
</section>
<section class="support-date">
<SupportedVariablesPanel />
</section>
<div class="mt-4 flex flex-col">
<h3 class="font-medium text-immich-primary dark:text-immich-dark-primary text-base">
Template
</h3>
<div class="text-xs my-2">
<h4>PREVIEW</h4>
</div>
<p class="text-xs">
Approximately path length limit : <span
class="font-semibold text-immich-primary dark:text-immich-dark-primary"
>{parsedTemplate().length + user.id.length + 'UPLOAD_LOCATION'.length}</span
>/260
</p>
<p class="text-xs">
{user.id} is the user's ID
</p>
<p
class="text-xs p-4 bg-gray-200 dark:bg-gray-700 dark:text-immich-dark-fg py-2 rounded-lg mt-2"
>
<span class="text-immich-fg/25 dark:text-immich-dark-fg/50"
>UPLOAD_LOCATION/{user.id}</span
>/{parsedTemplate()}.jpeg
</p>
<form autocomplete="off" class="flex flex-col" on:submit|preventDefault>
<div class="flex flex-col my-2">
<label class="text-xs" for="presets">PRESET</label>
<select
class="text-sm bg-slate-200 p-2 rounded-lg mt-2 dark:bg-gray-600 hover:cursor-pointer"
name="presets"
id="preset-select"
bind:value={selectedPreset}
on:change={handlePresetSelection}
>
{#each templateOptions.presetOptions as preset}
<option value={preset}>{renderTemplate(preset)}</option>
{/each}
</select>
</div>
<div class="flex gap-2 align-bottom">
<SettingInputField
label="template"
required
inputType={SettingInputFieldType.TEXT}
bind:value={storageConfig.template}
isEdited={!(storageConfig.template === savedConfig.template)}
/>
<div class="flex-0">
<SettingInputField
label="Extension"
inputType={SettingInputFieldType.TEXT}
value={'.jpeg'}
disabled
/>
</div>
</div>
<SettingButtonsRow
on:reset={reset}
on:save={saveSetting}
on:reset-to-default={resetToDefault}
showResetToDefault={!_.isEqual(savedConfig, defaultConfig)}
/>
</form>
</div>
</div>
{/await}
</section>

View File

@ -0,0 +1,78 @@
<script lang="ts">
import { SystemConfigTemplateStorageOptionDto } from '@api';
import * as luxon from 'luxon';
export let options: SystemConfigTemplateStorageOptionDto;
const getLuxonExample = (format: string) => {
return luxon.DateTime.fromISO(new Date('2022-09-04T20:03:05.250').toISOString()).toFormat(
format
);
};
</script>
<div class="text-xs mt-2">
<h4>DATE & TIME</h4>
</div>
<div class="text-xs bg-gray-200 dark:bg-gray-700 dark:text-immich-dark-fg p-4 mt-2 rounded-lg">
<div class="mb-2 text-gray-600 dark:text-immich-dark-fg">
<p>Asset's creation timestamp is used for the datetime information</p>
<p>Sample time 2022-09-04T20:03:05.250</p>
</div>
<div class="flex gap-[50px]">
<div>
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">YEAR</p>
<ul>
{#each options.yearOptions as yearFormat}
<li>{'{{'}{yearFormat}{'}}'} - {getLuxonExample(yearFormat)}</li>
{/each}
</ul>
</div>
<div>
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">MONTH</p>
<ul>
{#each options.monthOptions as monthFormat}
<li>{'{{'}{monthFormat}{'}}'} - {getLuxonExample(monthFormat)}</li>
{/each}
</ul>
</div>
<div>
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">DAY</p>
<ul>
{#each options.dayOptions as dayFormat}
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
{/each}
</ul>
</div>
<div>
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">HOUR</p>
<ul>
{#each options.hourOptions as dayFormat}
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
{/each}
</ul>
</div>
<div>
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">MINUTE</p>
<ul>
{#each options.minuteOptions as dayFormat}
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
{/each}
</ul>
</div>
<div>
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">SECOND</p>
<ul>
{#each options.secondOptions as dayFormat}
<li>{'{{'}{dayFormat}{'}}'} - {getLuxonExample(dayFormat)}</li>
{/each}
</ul>
</div>
</div>
</div>

View File

@ -0,0 +1,21 @@
<div class="text-xs mt-4">
<h4>OTHER VARIABLES</h4>
</div>
<div class="text-xs bg-gray-200 dark:bg-gray-700 dark:text-immich-dark-fg p-4 mt-2 rounded-lg">
<div class="flex gap-[50px]">
<div>
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">FILE NAME</p>
<ul>
<li>{`{{filename}}`}</li>
</ul>
</div>
<div>
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">FILE EXTENSION</p>
<ul>
<li>{`{{ext}}`}</li>
</ul>
</div>
</div>
</div>

View File

@ -2,11 +2,13 @@
import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte';
import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte';
import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte';
import StorageTemplateSettings from '$lib/components/admin-page/settings/storate-template/storage-template-settings.svelte';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { api, SystemConfigDto } from '@api';
import type { PageData } from './$types';
let systemConfig: SystemConfigDto;
export let data: PageData;
const getConfig = async () => {
const { data } = await api.systemConfigApi.getConfig();
systemConfig = data;
@ -33,5 +35,12 @@
<SettingAccordion title="OAuth Settings" subtitle="Manage the OAuth integration to Immich app">
<OAuthSettings oauthConfig={configs.oauth} />
</SettingAccordion>
<SettingAccordion
title="Storage Template"
subtitle="Manage the folder structure and file name of the upload asset"
>
<StorageTemplateSettings storageConfig={configs.storageTemplate} user={data.user} />
</SettingAccordion>
{/await}
</section>

View File

@ -22,7 +22,6 @@
onMount(() => {
allUsers = $page.data.allUsers;
console.log('getting all users', allUsers);
});
const isDeleted = (user: UserResponseDto): boolean => {