mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
parent
7c2f7d6c51
commit
f55b3add80
242 changed files with 12794 additions and 13426 deletions
|
|
@ -1,22 +1,22 @@
|
|||
<script lang="ts">
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
import ConfirmDialogue from '$lib/components/shared-components/confirm-dialogue.svelte';
|
||||
</script>
|
||||
|
||||
<ConfirmDialogue title="Disable Login" on:cancel on:confirm>
|
||||
<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>
|
||||
<p>
|
||||
To re-enable, use a
|
||||
<a
|
||||
href="https://immich.app/docs/administration/server-commands"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
class="underline"
|
||||
>
|
||||
Server Command</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<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>
|
||||
<p>
|
||||
To re-enable, use a
|
||||
<a
|
||||
href="https://immich.app/docs/administration/server-commands"
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
class="underline"
|
||||
>
|
||||
Server Command</a
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</ConfirmDialogue>
|
||||
|
|
|
|||
|
|
@ -1,211 +1,211 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { api, SystemConfigFFmpegDto, SystemConfigFFmpegDtoTranscodeEnum } from '@api';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
import SettingSelect from '../setting-select.svelte';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { api, SystemConfigFFmpegDto, SystemConfigFFmpegDtoTranscodeEnum } from '@api';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
import SettingSelect from '../setting-select.svelte';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
export let ffmpegConfig: SystemConfigFFmpegDto; // this is the config that is being edited
|
||||
export let ffmpegConfig: SystemConfigFFmpegDto; // this is the config that is being edited
|
||||
|
||||
let savedConfig: SystemConfigFFmpegDto;
|
||||
let defaultConfig: SystemConfigFFmpegDto;
|
||||
let savedConfig: SystemConfigFFmpegDto;
|
||||
let defaultConfig: SystemConfigFFmpegDto;
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.ffmpeg),
|
||||
api.systemConfigApi.getDefaults().then((res) => res.data.ffmpeg)
|
||||
]);
|
||||
}
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.ffmpeg),
|
||||
api.systemConfigApi.getDefaults().then((res) => res.data.ffmpeg),
|
||||
]);
|
||||
}
|
||||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: configs } = await api.systemConfigApi.getConfig();
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: configs } = await api.systemConfigApi.getConfig();
|
||||
|
||||
const result = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...configs,
|
||||
ffmpeg: ffmpegConfig
|
||||
}
|
||||
});
|
||||
const result = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...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',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error [ffmpeg-settings] [saveSetting]', e);
|
||||
notificationController.show({
|
||||
message: 'Unable to save settings',
|
||||
type: NotificationType.Error
|
||||
});
|
||||
}
|
||||
}
|
||||
notificationController.show({
|
||||
message: 'FFmpeg settings saved',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error [ffmpeg-settings] [saveSetting]', e);
|
||||
notificationController.show({
|
||||
message: 'Unable to save settings',
|
||||
type: NotificationType.Error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
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',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
}
|
||||
notificationController.show({
|
||||
message: 'Reset FFmpeg settings to the recent saved settings',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getDefaults();
|
||||
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',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
}
|
||||
notificationController.show({
|
||||
message: 'Reset FFmpeg settings to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="flex flex-col gap-4 ml-4 mt-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="CONSTANT RATE FACTOR (-crf)"
|
||||
desc="Video quality level. Typical values are 23 for H.264, 28 for HEVC, and 31 for VP9. Lower is better, but takes longer to encode and produces larger files."
|
||||
bind:value={ffmpegConfig.crf}
|
||||
required={true}
|
||||
isEdited={!(ffmpegConfig.crf == savedConfig.crf)}
|
||||
/>
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="flex flex-col gap-4 ml-4 mt-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="CONSTANT RATE FACTOR (-crf)"
|
||||
desc="Video quality level. Typical values are 23 for H.264, 28 for HEVC, and 31 for VP9. Lower is better, but takes longer to encode and produces larger files."
|
||||
bind:value={ffmpegConfig.crf}
|
||||
required={true}
|
||||
isEdited={!(ffmpegConfig.crf == savedConfig.crf)}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="PRESET (-preset)"
|
||||
desc="Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above `faster`."
|
||||
bind:value={ffmpegConfig.preset}
|
||||
name="preset"
|
||||
options={[
|
||||
{ value: 'ultrafast', text: 'ultrafast' },
|
||||
{ value: 'superfast', text: 'superfast' },
|
||||
{ value: 'veryfast', text: 'veryfast' },
|
||||
{ value: 'faster', text: 'faster' },
|
||||
{ value: 'fast', text: 'fast' },
|
||||
{ value: 'medium', text: 'medium' },
|
||||
{ value: 'slow', text: 'slow' },
|
||||
{ value: 'slower', text: 'slower' },
|
||||
{ value: 'veryslow', text: 'veryslow' }
|
||||
]}
|
||||
isEdited={!(ffmpegConfig.preset == savedConfig.preset)}
|
||||
/>
|
||||
<SettingSelect
|
||||
label="PRESET (-preset)"
|
||||
desc="Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above `faster`."
|
||||
bind:value={ffmpegConfig.preset}
|
||||
name="preset"
|
||||
options={[
|
||||
{ value: 'ultrafast', text: 'ultrafast' },
|
||||
{ value: 'superfast', text: 'superfast' },
|
||||
{ value: 'veryfast', text: 'veryfast' },
|
||||
{ value: 'faster', text: 'faster' },
|
||||
{ value: 'fast', text: 'fast' },
|
||||
{ value: 'medium', text: 'medium' },
|
||||
{ value: 'slow', text: 'slow' },
|
||||
{ value: 'slower', text: 'slower' },
|
||||
{ value: 'veryslow', text: 'veryslow' },
|
||||
]}
|
||||
isEdited={!(ffmpegConfig.preset == savedConfig.preset)}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="AUDIO CODEC"
|
||||
desc="Opus is the highest quality option, but has lower compatibility with old devices or software."
|
||||
bind:value={ffmpegConfig.targetAudioCodec}
|
||||
options={[
|
||||
{ value: 'aac', text: 'aac' },
|
||||
{ value: 'mp3', text: 'mp3' },
|
||||
{ value: 'opus', text: 'opus' }
|
||||
]}
|
||||
name="acodec"
|
||||
isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)}
|
||||
/>
|
||||
<SettingSelect
|
||||
label="AUDIO CODEC"
|
||||
desc="Opus is the highest quality option, but has lower compatibility with old devices or software."
|
||||
bind:value={ffmpegConfig.targetAudioCodec}
|
||||
options={[
|
||||
{ value: 'aac', text: 'aac' },
|
||||
{ value: 'mp3', text: 'mp3' },
|
||||
{ value: 'opus', text: 'opus' },
|
||||
]}
|
||||
name="acodec"
|
||||
isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="VIDEO CODEC"
|
||||
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."
|
||||
bind:value={ffmpegConfig.targetVideoCodec}
|
||||
options={[
|
||||
{ value: 'h264', text: 'h264' },
|
||||
{ value: 'hevc', text: 'hevc' },
|
||||
{ value: 'vp9', text: 'vp9' }
|
||||
]}
|
||||
name="vcodec"
|
||||
isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)}
|
||||
/>
|
||||
<SettingSelect
|
||||
label="VIDEO CODEC"
|
||||
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."
|
||||
bind:value={ffmpegConfig.targetVideoCodec}
|
||||
options={[
|
||||
{ value: 'h264', text: 'h264' },
|
||||
{ value: 'hevc', text: 'hevc' },
|
||||
{ value: 'vp9', text: 'vp9' },
|
||||
]}
|
||||
name="vcodec"
|
||||
isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="TARGET RESOLUTION"
|
||||
desc="Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
||||
bind:value={ffmpegConfig.targetResolution}
|
||||
options={[
|
||||
{ value: '2160', text: '4k' },
|
||||
{ value: '1440', text: '1440p' },
|
||||
{ value: '1080', text: '1080p' },
|
||||
{ value: '720', text: '720p' },
|
||||
{ value: '480', text: '480p' },
|
||||
{ value: 'original', text: 'original' }
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={!(ffmpegConfig.targetResolution == savedConfig.targetResolution)}
|
||||
/>
|
||||
<SettingSelect
|
||||
label="TARGET RESOLUTION"
|
||||
desc="Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
||||
bind:value={ffmpegConfig.targetResolution}
|
||||
options={[
|
||||
{ value: '2160', text: '4k' },
|
||||
{ value: '1440', text: '1440p' },
|
||||
{ value: '1080', text: '1080p' },
|
||||
{ value: '720', text: '720p' },
|
||||
{ value: '480', text: '480p' },
|
||||
{ value: 'original', text: 'original' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={!(ffmpegConfig.targetResolution == savedConfig.targetResolution)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
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."
|
||||
bind:value={ffmpegConfig.maxBitrate}
|
||||
isEdited={!(ffmpegConfig.maxBitrate == savedConfig.maxBitrate)}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
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."
|
||||
bind:value={ffmpegConfig.maxBitrate}
|
||||
isEdited={!(ffmpegConfig.maxBitrate == savedConfig.maxBitrate)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
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."
|
||||
bind:value={ffmpegConfig.threads}
|
||||
isEdited={!(ffmpegConfig.threads == savedConfig.threads)}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
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."
|
||||
bind:value={ffmpegConfig.threads}
|
||||
isEdited={!(ffmpegConfig.threads == savedConfig.threads)}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="TRANSCODE"
|
||||
desc="Policy for when a video should be transcoded."
|
||||
bind:value={ffmpegConfig.transcode}
|
||||
name="transcode"
|
||||
options={[
|
||||
{ value: SystemConfigFFmpegDtoTranscodeEnum.All, text: 'All videos' },
|
||||
{
|
||||
value: SystemConfigFFmpegDtoTranscodeEnum.Optimal,
|
||||
text: 'Videos higher than target resolution or not in the desired format'
|
||||
},
|
||||
{
|
||||
value: SystemConfigFFmpegDtoTranscodeEnum.Required,
|
||||
text: 'Only videos not in the desired format'
|
||||
},
|
||||
{
|
||||
value: SystemConfigFFmpegDtoTranscodeEnum.Disabled,
|
||||
text: "Don't transcode any videos, may break playback on some clients"
|
||||
}
|
||||
]}
|
||||
isEdited={!(ffmpegConfig.transcode == savedConfig.transcode)}
|
||||
/>
|
||||
<SettingSelect
|
||||
label="TRANSCODE"
|
||||
desc="Policy for when a video should be transcoded."
|
||||
bind:value={ffmpegConfig.transcode}
|
||||
name="transcode"
|
||||
options={[
|
||||
{ value: SystemConfigFFmpegDtoTranscodeEnum.All, text: 'All videos' },
|
||||
{
|
||||
value: SystemConfigFFmpegDtoTranscodeEnum.Optimal,
|
||||
text: 'Videos higher than target resolution or not in the desired format',
|
||||
},
|
||||
{
|
||||
value: SystemConfigFFmpegDtoTranscodeEnum.Required,
|
||||
text: 'Only videos not in the desired format',
|
||||
},
|
||||
{
|
||||
value: SystemConfigFFmpegDtoTranscodeEnum.Disabled,
|
||||
text: "Don't transcode any videos, may break playback on some clients",
|
||||
},
|
||||
]}
|
||||
isEdited={!(ffmpegConfig.transcode == savedConfig.transcode)}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="TWO-PASS ENCODING"
|
||||
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."
|
||||
bind:checked={ffmpegConfig.twoPass}
|
||||
isEdited={!(ffmpegConfig.twoPass === savedConfig.twoPass)}
|
||||
/>
|
||||
</div>
|
||||
<SettingSwitch
|
||||
title="TWO-PASS ENCODING"
|
||||
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."
|
||||
bind:checked={ffmpegConfig.twoPass}
|
||||
isEdited={!(ffmpegConfig.twoPass === savedConfig.twoPass)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,103 +1,101 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { api, JobName, SystemConfigJobDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { handleError } from '../../../../utils/handle-error';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { api, JobName, SystemConfigJobDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { handleError } from '../../../../utils/handle-error';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
|
||||
export let jobConfig: SystemConfigJobDto; // this is the config that is being edited
|
||||
export let jobConfig: SystemConfigJobDto; // this is the config that is being edited
|
||||
|
||||
let savedConfig: SystemConfigJobDto;
|
||||
let defaultConfig: SystemConfigJobDto;
|
||||
let savedConfig: SystemConfigJobDto;
|
||||
let defaultConfig: SystemConfigJobDto;
|
||||
|
||||
const ignoredJobs = [JobName.BackgroundTask, JobName.Search] as JobName[];
|
||||
const jobNames = Object.values(JobName).filter(
|
||||
(jobName) => !ignoredJobs.includes(jobName as JobName)
|
||||
);
|
||||
const ignoredJobs = [JobName.BackgroundTask, JobName.Search] as JobName[];
|
||||
const jobNames = Object.values(JobName).filter((jobName) => !ignoredJobs.includes(jobName as JobName));
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.job),
|
||||
api.systemConfigApi.getDefaults().then((res) => res.data.job)
|
||||
]);
|
||||
}
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.job),
|
||||
api.systemConfigApi.getDefaults().then((res) => res.data.job),
|
||||
]);
|
||||
}
|
||||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: configs } = await api.systemConfigApi.getConfig();
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: configs } = await api.systemConfigApi.getConfig();
|
||||
|
||||
const result = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...configs,
|
||||
job: jobConfig
|
||||
}
|
||||
});
|
||||
const result = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...configs,
|
||||
job: jobConfig,
|
||||
},
|
||||
});
|
||||
|
||||
jobConfig = { ...result.data.job };
|
||||
savedConfig = { ...result.data.job };
|
||||
jobConfig = { ...result.data.job };
|
||||
savedConfig = { ...result.data.job };
|
||||
|
||||
notificationController.show({ message: 'Job settings saved', type: NotificationType.Info });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save settings');
|
||||
}
|
||||
}
|
||||
notificationController.show({ message: 'Job settings saved', type: NotificationType.Info });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save settings');
|
||||
}
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
jobConfig = { ...resetConfig.job };
|
||||
savedConfig = { ...resetConfig.job };
|
||||
jobConfig = { ...resetConfig.job };
|
||||
savedConfig = { ...resetConfig.job };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset Job settings to the recent saved settings',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
}
|
||||
notificationController.show({
|
||||
message: 'Reset Job settings to the recent saved settings',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getDefaults();
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getDefaults();
|
||||
|
||||
jobConfig = { ...configs.job };
|
||||
defaultConfig = { ...configs.job };
|
||||
jobConfig = { ...configs.job };
|
||||
defaultConfig = { ...configs.job };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset Job settings to default',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
}
|
||||
notificationController.show({
|
||||
message: 'Reset Job settings to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
{#each jobNames as jobName}
|
||||
<div class="flex flex-col gap-4 ml-4 mt-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="{api.getJobName(jobName)} Concurrency"
|
||||
desc=""
|
||||
bind:value={jobConfig[jobName].concurrency}
|
||||
required={true}
|
||||
isEdited={!(jobConfig[jobName].concurrency == savedConfig[jobName].concurrency)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
{#each jobNames as jobName}
|
||||
<div class="flex flex-col gap-4 ml-4 mt-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="{api.getJobName(jobName)} Concurrency"
|
||||
desc=""
|
||||
bind:value={jobConfig[jobName].concurrency}
|
||||
required={true}
|
||||
isEdited={!(jobConfig[jobName].concurrency == savedConfig[jobName].concurrency)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,212 +1,209 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, SystemConfigOAuthDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import ConfirmDisableLogin from '../confirm-disable-login.svelte';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, SystemConfigOAuthDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import ConfirmDisableLogin from '../confirm-disable-login.svelte';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
|
||||
export let oauthConfig: SystemConfigOAuthDto;
|
||||
export let oauthConfig: SystemConfigOAuthDto;
|
||||
|
||||
let savedConfig: SystemConfigOAuthDto;
|
||||
let defaultConfig: SystemConfigOAuthDto;
|
||||
let savedConfig: SystemConfigOAuthDto;
|
||||
let defaultConfig: SystemConfigOAuthDto;
|
||||
|
||||
const handleToggleOverride = () => {
|
||||
// click runs before bind
|
||||
const previouslyEnabled = oauthConfig.mobileOverrideEnabled;
|
||||
if (!previouslyEnabled && !oauthConfig.mobileRedirectUri) {
|
||||
oauthConfig.mobileRedirectUri = window.location.origin + '/api/oauth/mobile-redirect';
|
||||
}
|
||||
};
|
||||
const handleToggleOverride = () => {
|
||||
// click runs before bind
|
||||
const previouslyEnabled = oauthConfig.mobileOverrideEnabled;
|
||||
if (!previouslyEnabled && !oauthConfig.mobileRedirectUri) {
|
||||
oauthConfig.mobileRedirectUri = window.location.origin + '/api/oauth/mobile-redirect';
|
||||
}
|
||||
};
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.oauth),
|
||||
api.systemConfigApi.getDefaults().then((res) => res.data.oauth)
|
||||
]);
|
||||
}
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.oauth),
|
||||
api.systemConfigApi.getDefaults().then((res) => res.data.oauth),
|
||||
]);
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
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',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
}
|
||||
notificationController.show({
|
||||
message: 'Reset OAuth settings to the last saved settings',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
let isConfirmOpen = false;
|
||||
let handleConfirm: (value: boolean) => void;
|
||||
let isConfirmOpen = false;
|
||||
let handleConfirm: (value: boolean) => void;
|
||||
|
||||
const openConfirmModal = () => {
|
||||
return new Promise((resolve) => {
|
||||
handleConfirm = (value: boolean) => {
|
||||
isConfirmOpen = false;
|
||||
resolve(value);
|
||||
};
|
||||
isConfirmOpen = true;
|
||||
});
|
||||
};
|
||||
const openConfirmModal = () => {
|
||||
return new Promise((resolve) => {
|
||||
handleConfirm = (value: boolean) => {
|
||||
isConfirmOpen = false;
|
||||
resolve(value);
|
||||
};
|
||||
isConfirmOpen = true;
|
||||
});
|
||||
};
|
||||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: current } = await api.systemConfigApi.getConfig();
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: current } = await api.systemConfigApi.getConfig();
|
||||
|
||||
if (!current.passwordLogin.enabled && current.oauth.enabled && !oauthConfig.enabled) {
|
||||
const confirmed = await openConfirmModal();
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!current.passwordLogin.enabled && current.oauth.enabled && !oauthConfig.enabled) {
|
||||
const confirmed = await openConfirmModal();
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!oauthConfig.mobileOverrideEnabled) {
|
||||
oauthConfig.mobileRedirectUri = '';
|
||||
}
|
||||
if (!oauthConfig.mobileOverrideEnabled) {
|
||||
oauthConfig.mobileRedirectUri = '';
|
||||
}
|
||||
|
||||
const { data: updated } = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...current,
|
||||
oauth: oauthConfig
|
||||
}
|
||||
});
|
||||
const { data: updated } = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...current,
|
||||
oauth: oauthConfig,
|
||||
},
|
||||
});
|
||||
|
||||
oauthConfig = { ...updated.oauth };
|
||||
savedConfig = { ...updated.oauth };
|
||||
oauthConfig = { ...updated.oauth };
|
||||
savedConfig = { ...updated.oauth };
|
||||
|
||||
notificationController.show({ message: 'OAuth settings saved', type: NotificationType.Info });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save OAuth settings');
|
||||
}
|
||||
}
|
||||
notificationController.show({ message: 'OAuth settings saved', type: NotificationType.Info });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save OAuth settings');
|
||||
}
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: defaultConfig } = await api.systemConfigApi.getDefaults();
|
||||
async function resetToDefault() {
|
||||
const { data: defaultConfig } = await api.systemConfigApi.getDefaults();
|
||||
|
||||
oauthConfig = { ...defaultConfig.oauth };
|
||||
oauthConfig = { ...defaultConfig.oauth };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset OAuth settings to default',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
}
|
||||
notificationController.show({
|
||||
message: 'Reset OAuth settings to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isConfirmOpen}
|
||||
<ConfirmDisableLogin
|
||||
on:cancel={() => handleConfirm(false)}
|
||||
on:confirm={() => handleConfirm(true)}
|
||||
/>
|
||||
<ConfirmDisableLogin on:cancel={() => handleConfirm(false)} on:confirm={() => handleConfirm(true)} />
|
||||
{/if}
|
||||
|
||||
<div class="mt-2">
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault class="flex flex-col mx-4 gap-4 py-4">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
For more details about this feature, refer to the <a
|
||||
href="http://immich.app/docs/administration/oauth#mobile-redirect-uri"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer">docs</a
|
||||
>.
|
||||
</p>
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault class="flex flex-col mx-4 gap-4 py-4">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
For more details about this feature, refer to the <a
|
||||
href="http://immich.app/docs/administration/oauth#mobile-redirect-uri"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer">docs</a
|
||||
>.
|
||||
</p>
|
||||
|
||||
<SettingSwitch title="ENABLE" bind:checked={oauthConfig.enabled} />
|
||||
<hr />
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="ISSUER URL"
|
||||
bind:value={oauthConfig.issuerUrl}
|
||||
required={true}
|
||||
disabled={!oauthConfig.enabled}
|
||||
isEdited={!(oauthConfig.issuerUrl == savedConfig.issuerUrl)}
|
||||
/>
|
||||
<SettingSwitch title="ENABLE" bind:checked={oauthConfig.enabled} />
|
||||
<hr />
|
||||
<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)}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="AUTO REGISTER"
|
||||
subtitle="Automatically register new users after signing in with OAuth"
|
||||
bind:checked={oauthConfig.autoRegister}
|
||||
disabled={!oauthConfig.enabled}
|
||||
/>
|
||||
<SettingSwitch
|
||||
title="AUTO REGISTER"
|
||||
subtitle="Automatically register new users after signing in with OAuth"
|
||||
bind:checked={oauthConfig.autoRegister}
|
||||
disabled={!oauthConfig.enabled}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="AUTO LAUNCH"
|
||||
subtitle="Start the OAuth login flow automatically upon navigating to the login page"
|
||||
disabled={!oauthConfig.enabled}
|
||||
bind:checked={oauthConfig.autoLaunch}
|
||||
/>
|
||||
<SettingSwitch
|
||||
title="AUTO LAUNCH"
|
||||
subtitle="Start the OAuth login flow automatically upon navigating to the login page"
|
||||
disabled={!oauthConfig.enabled}
|
||||
bind:checked={oauthConfig.autoLaunch}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="MOBILE REDIRECT URI OVERRIDE"
|
||||
subtitle="Enable when `app.immich:/` is an invalid redirect URI."
|
||||
disabled={!oauthConfig.enabled}
|
||||
on:click={() => handleToggleOverride()}
|
||||
bind:checked={oauthConfig.mobileOverrideEnabled}
|
||||
/>
|
||||
<SettingSwitch
|
||||
title="MOBILE REDIRECT URI OVERRIDE"
|
||||
subtitle="Enable when `app.immich:/` is an invalid redirect URI."
|
||||
disabled={!oauthConfig.enabled}
|
||||
on:click={() => handleToggleOverride()}
|
||||
bind:checked={oauthConfig.mobileOverrideEnabled}
|
||||
/>
|
||||
|
||||
{#if oauthConfig.mobileOverrideEnabled}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="MOBILE REDIRECT URI"
|
||||
bind:value={oauthConfig.mobileRedirectUri}
|
||||
required={true}
|
||||
disabled={!oauthConfig.enabled}
|
||||
isEdited={!(oauthConfig.mobileRedirectUri == savedConfig.mobileRedirectUri)}
|
||||
/>
|
||||
{/if}
|
||||
{#if oauthConfig.mobileOverrideEnabled}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="MOBILE REDIRECT URI"
|
||||
bind:value={oauthConfig.mobileRedirectUri}
|
||||
required={true}
|
||||
disabled={!oauthConfig.enabled}
|
||||
isEdited={!(oauthConfig.mobileRedirectUri == savedConfig.mobileRedirectUri)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,121 +1,118 @@
|
|||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, SystemConfigPasswordLoginDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import ConfirmDisableLogin from '../confirm-disable-login.svelte';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, SystemConfigPasswordLoginDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import ConfirmDisableLogin from '../confirm-disable-login.svelte';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
|
||||
export let passwordLoginConfig: SystemConfigPasswordLoginDto; // this is the config that is being edited
|
||||
export let passwordLoginConfig: SystemConfigPasswordLoginDto; // this is the config that is being edited
|
||||
|
||||
let savedConfig: SystemConfigPasswordLoginDto;
|
||||
let defaultConfig: SystemConfigPasswordLoginDto;
|
||||
let savedConfig: SystemConfigPasswordLoginDto;
|
||||
let defaultConfig: SystemConfigPasswordLoginDto;
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.passwordLogin),
|
||||
api.systemConfigApi.getDefaults().then((res) => res.data.passwordLogin)
|
||||
]);
|
||||
}
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.passwordLogin),
|
||||
api.systemConfigApi.getDefaults().then((res) => res.data.passwordLogin),
|
||||
]);
|
||||
}
|
||||
|
||||
let isConfirmOpen = false;
|
||||
let handleConfirm: (value: boolean) => void;
|
||||
let isConfirmOpen = false;
|
||||
let handleConfirm: (value: boolean) => void;
|
||||
|
||||
const openConfirmModal = () => {
|
||||
return new Promise((resolve) => {
|
||||
handleConfirm = (value: boolean) => {
|
||||
isConfirmOpen = false;
|
||||
resolve(value);
|
||||
};
|
||||
isConfirmOpen = true;
|
||||
});
|
||||
};
|
||||
const openConfirmModal = () => {
|
||||
return new Promise((resolve) => {
|
||||
handleConfirm = (value: boolean) => {
|
||||
isConfirmOpen = false;
|
||||
resolve(value);
|
||||
};
|
||||
isConfirmOpen = true;
|
||||
});
|
||||
};
|
||||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: current } = await api.systemConfigApi.getConfig();
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: current } = await api.systemConfigApi.getConfig();
|
||||
|
||||
if (!current.oauth.enabled && current.passwordLogin.enabled && !passwordLoginConfig.enabled) {
|
||||
const confirmed = await openConfirmModal();
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!current.oauth.enabled && current.passwordLogin.enabled && !passwordLoginConfig.enabled) {
|
||||
const confirmed = await openConfirmModal();
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { data: updated } = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...current,
|
||||
passwordLogin: passwordLoginConfig
|
||||
}
|
||||
});
|
||||
const { data: updated } = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...current,
|
||||
passwordLogin: passwordLoginConfig,
|
||||
},
|
||||
});
|
||||
|
||||
passwordLoginConfig = { ...updated.passwordLogin };
|
||||
savedConfig = { ...updated.passwordLogin };
|
||||
passwordLoginConfig = { ...updated.passwordLogin };
|
||||
savedConfig = { ...updated.passwordLogin };
|
||||
|
||||
notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save settings');
|
||||
}
|
||||
}
|
||||
notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save settings');
|
||||
}
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
passwordLoginConfig = { ...resetConfig.passwordLogin };
|
||||
savedConfig = { ...resetConfig.passwordLogin };
|
||||
passwordLoginConfig = { ...resetConfig.passwordLogin };
|
||||
savedConfig = { ...resetConfig.passwordLogin };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset settings to the recent saved settings',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
}
|
||||
notificationController.show({
|
||||
message: 'Reset settings to the recent saved settings',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getDefaults();
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getDefaults();
|
||||
|
||||
passwordLoginConfig = { ...configs.passwordLogin };
|
||||
defaultConfig = { ...configs.passwordLogin };
|
||||
passwordLoginConfig = { ...configs.passwordLogin };
|
||||
defaultConfig = { ...configs.passwordLogin };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset password settings to default',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
}
|
||||
notificationController.show({
|
||||
message: 'Reset password settings to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isConfirmOpen}
|
||||
<ConfirmDisableLogin
|
||||
on:cancel={() => handleConfirm(false)}
|
||||
on:confirm={() => handleConfirm(true)}
|
||||
/>
|
||||
<ConfirmDisableLogin on:cancel={() => handleConfirm(false)} on:confirm={() => handleConfirm(true)} />
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="flex flex-col gap-4 ml-4 mt-4">
|
||||
<div class="ml-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
subtitle="Login with email and password"
|
||||
bind:checked={passwordLoginConfig.enabled}
|
||||
/>
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="flex flex-col gap-4 ml-4 mt-4">
|
||||
<div class="ml-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
subtitle="Login with email and password"
|
||||
bind:checked={passwordLoginConfig.enabled}
|
||||
/>
|
||||
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,56 +1,56 @@
|
|||
<script lang="ts">
|
||||
import { slide } from 'svelte/transition';
|
||||
export let title: string;
|
||||
export let subtitle = '';
|
||||
import { slide } from 'svelte/transition';
|
||||
export let title: string;
|
||||
export let subtitle = '';
|
||||
|
||||
export let isOpen = false;
|
||||
const toggle = () => (isOpen = !isOpen);
|
||||
export let isOpen = false;
|
||||
const toggle = () => (isOpen = !isOpen);
|
||||
</script>
|
||||
|
||||
<div class="border-b-[1px] border-gray-200 dark:border-gray-700 py-4">
|
||||
<div class="flex justify-between place-items-center">
|
||||
<div>
|
||||
<h2 class="font-medium text-immich-primary dark:text-immich-dark-primary">
|
||||
{title}
|
||||
</h2>
|
||||
<div class="flex justify-between place-items-center">
|
||||
<div>
|
||||
<h2 class="font-medium text-immich-primary dark:text-immich-dark-primary">
|
||||
{title}
|
||||
</h2>
|
||||
|
||||
<p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
|
||||
</div>
|
||||
<p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
|
||||
</div>
|
||||
|
||||
<button
|
||||
on:click={toggle}
|
||||
aria-expanded={isOpen}
|
||||
class="immich-circle-icon-button hover:bg-immich-primary/10 dark:text-immich-dark-fg hover:dark:bg-immich-dark-primary/20 rounded-full p-3 flex place-items-center place-content-center transition-all"
|
||||
>
|
||||
<svg
|
||||
style="tran"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
on:click={toggle}
|
||||
aria-expanded={isOpen}
|
||||
class="immich-circle-icon-button hover:bg-immich-primary/10 dark:text-immich-dark-fg hover:dark:bg-immich-dark-primary/20 rounded-full p-3 flex place-items-center place-content-center transition-all"
|
||||
>
|
||||
<svg
|
||||
style="tran"
|
||||
width="20"
|
||||
height="20"
|
||||
fill="none"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{#if isOpen}
|
||||
<ul transition:slide={{ duration: 250 }} class="mb-2 ml-4">
|
||||
<slot />
|
||||
</ul>
|
||||
{/if}
|
||||
{#if isOpen}
|
||||
<ul transition:slide={{ duration: 250 }} class="mb-2 ml-4">
|
||||
<slot />
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
svg {
|
||||
transition: transform 0.2s ease-in;
|
||||
}
|
||||
svg {
|
||||
transition: transform 0.2s ease-in;
|
||||
}
|
||||
|
||||
[aria-expanded='true'] svg {
|
||||
transform: rotate(0.5turn);
|
||||
}
|
||||
[aria-expanded='true'] svg {
|
||||
transform: rotate(0.5turn);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,26 +1,26 @@
|
|||
<script lang="ts">
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let showResetToDefault = true;
|
||||
export let showResetToDefault = true;
|
||||
</script>
|
||||
|
||||
<div class="flex justify-between gap-2 mt-8">
|
||||
<div class="left">
|
||||
{#if showResetToDefault}
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="left">
|
||||
{#if showResetToDefault}
|
||||
<button
|
||||
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
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="right">
|
||||
<Button size="sm" color="gray" on:click={() => dispatch('reset')}>Reset</Button>
|
||||
<Button size="sm" on:click={() => dispatch('save')}>Save</Button>
|
||||
</div>
|
||||
<div class="right">
|
||||
<Button size="sm" color="gray" on:click={() => dispatch('reset')}>Reset</Button>
|
||||
<Button size="sm" on:click={() => dispatch('save')}>Save</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,65 +1,65 @@
|
|||
<script lang="ts" context="module">
|
||||
export enum SettingInputFieldType {
|
||||
EMAIL = 'email',
|
||||
TEXT = 'text',
|
||||
NUMBER = 'number',
|
||||
PASSWORD = 'password'
|
||||
}
|
||||
export enum SettingInputFieldType {
|
||||
EMAIL = 'email',
|
||||
TEXT = 'text',
|
||||
NUMBER = 'number',
|
||||
PASSWORD = 'password',
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
export let inputType: SettingInputFieldType;
|
||||
export let value: string | number;
|
||||
export let label = '';
|
||||
export let desc = '';
|
||||
export let required = false;
|
||||
export let disabled = false;
|
||||
export let isEdited = false;
|
||||
export let inputType: SettingInputFieldType;
|
||||
export let value: string | number;
|
||||
export let label = '';
|
||||
export let desc = '';
|
||||
export let required = false;
|
||||
export let disabled = false;
|
||||
export let isEdited = false;
|
||||
|
||||
const handleInput = (e: Event) => {
|
||||
value = (e.target as HTMLInputElement).value;
|
||||
if (inputType === SettingInputFieldType.NUMBER) {
|
||||
value = Number(value) || 0;
|
||||
}
|
||||
};
|
||||
const handleInput = (e: Event) => {
|
||||
value = (e.target as HTMLInputElement).value;
|
||||
if (inputType === SettingInputFieldType.NUMBER) {
|
||||
value = Number(value) || 0;
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<div class={`flex place-items-center gap-1 h-[26px]`}>
|
||||
<label class={`immich-form-label text-sm`} for={label}>{label}</label>
|
||||
{#if required}
|
||||
<div class="text-red-400">*</div>
|
||||
{/if}
|
||||
<div class={`flex place-items-center gap-1 h-[26px]`}>
|
||||
<label class={`immich-form-label text-sm`} for={label}>{label}</label>
|
||||
{#if required}
|
||||
<div class="text-red-400">*</div>
|
||||
{/if}
|
||||
|
||||
{#if isEdited}
|
||||
<div
|
||||
transition:fly={{ x: 10, duration: 200, easing: quintOut }}
|
||||
class="bg-orange-100 px-2 rounded-full text-orange-900 text-[10px]"
|
||||
>
|
||||
Unsaved change
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if isEdited}
|
||||
<div
|
||||
transition:fly={{ x: 10, duration: 200, easing: quintOut }}
|
||||
class="bg-orange-100 px-2 rounded-full text-orange-900 text-[10px]"
|
||||
>
|
||||
Unsaved change
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if desc}
|
||||
<p class="immich-form-label text-xs pb-2" id="{label}-desc">
|
||||
{desc}
|
||||
</p>
|
||||
{/if}
|
||||
{#if desc}
|
||||
<p class="immich-form-label text-xs pb-2" id="{label}-desc">
|
||||
{desc}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<input
|
||||
class="immich-form-input pb-2 w-full"
|
||||
aria-describedby={desc ? `${label}-desc` : undefined}
|
||||
aria-labelledby="{label}-label"
|
||||
id={label}
|
||||
name={label}
|
||||
type={inputType}
|
||||
{required}
|
||||
{value}
|
||||
on:input={handleInput}
|
||||
{disabled}
|
||||
/>
|
||||
<input
|
||||
class="immich-form-input pb-2 w-full"
|
||||
aria-describedby={desc ? `${label}-desc` : undefined}
|
||||
aria-labelledby="{label}-label"
|
||||
id={label}
|
||||
name={label}
|
||||
type={inputType}
|
||||
{required}
|
||||
{value}
|
||||
on:input={handleInput}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,49 +1,49 @@
|
|||
<script lang="ts">
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
export let value: string;
|
||||
export let options: { value: string; text: string }[];
|
||||
export let label = '';
|
||||
export let desc = '';
|
||||
export let name = '';
|
||||
export let isEdited = false;
|
||||
export let value: string;
|
||||
export let options: { value: string; text: string }[];
|
||||
export let label = '';
|
||||
export let desc = '';
|
||||
export let name = '';
|
||||
export let isEdited = false;
|
||||
|
||||
const handleChange = (e: Event) => {
|
||||
value = (e.target as HTMLInputElement).value;
|
||||
};
|
||||
const handleChange = (e: Event) => {
|
||||
value = (e.target as HTMLInputElement).value;
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="w-full">
|
||||
<div class={`flex place-items-center gap-1 h-[26px]`}>
|
||||
<label class={`immich-form-label text-sm`} for="{name}-select">{label}</label>
|
||||
<div class={`flex place-items-center gap-1 h-[26px]`}>
|
||||
<label class={`immich-form-label text-sm`} for="{name}-select">{label}</label>
|
||||
|
||||
{#if isEdited}
|
||||
<div
|
||||
transition:fly={{ x: 10, duration: 200, easing: quintOut }}
|
||||
class="bg-orange-100 px-2 rounded-full text-orange-900 text-[10px]"
|
||||
>
|
||||
Unsaved change
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{#if isEdited}
|
||||
<div
|
||||
transition:fly={{ x: 10, duration: 200, easing: quintOut }}
|
||||
class="bg-orange-100 px-2 rounded-full text-orange-900 text-[10px]"
|
||||
>
|
||||
Unsaved change
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if desc}
|
||||
<p class="immich-form-label text-xs pb-2" id="{name}-desc">
|
||||
{desc}
|
||||
</p>
|
||||
{/if}
|
||||
{#if desc}
|
||||
<p class="immich-form-label text-xs pb-2" id="{name}-desc">
|
||||
{desc}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<select
|
||||
class="immich-form-input pb-2 w-full"
|
||||
aria-describedby={desc ? `${name}-desc` : undefined}
|
||||
{name}
|
||||
id="{name}-select"
|
||||
bind:value
|
||||
on:change={handleChange}
|
||||
>
|
||||
{#each options as option}
|
||||
<option value={option.value}>{option.text}</option>
|
||||
{/each}
|
||||
</select>
|
||||
<select
|
||||
class="immich-form-input pb-2 w-full"
|
||||
aria-describedby={desc ? `${name}-desc` : undefined}
|
||||
{name}
|
||||
id="{name}-select"
|
||||
bind:value
|
||||
on:change={handleChange}
|
||||
>
|
||||
{#each options as option}
|
||||
<option value={option.value}>{option.text}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,96 +1,90 @@
|
|||
<script lang="ts">
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
export let title: string;
|
||||
export let subtitle = '';
|
||||
export let checked = false;
|
||||
export let disabled = false;
|
||||
export let isEdited = false;
|
||||
export let title: string;
|
||||
export let subtitle = '';
|
||||
export let checked = false;
|
||||
export let disabled = false;
|
||||
export let isEdited = false;
|
||||
</script>
|
||||
|
||||
<div class="flex justify-between place-items-center">
|
||||
<div>
|
||||
<div class="flex place-items-center gap-1 h-[26px]">
|
||||
<label class="immich-form-label text-sm" for={title}>
|
||||
{title}
|
||||
</label>
|
||||
{#if isEdited}
|
||||
<div
|
||||
transition:fly={{ x: 10, duration: 200, easing: quintOut }}
|
||||
class="bg-orange-100 px-2 rounded-full text-orange-900 text-[10px]"
|
||||
>
|
||||
Unsaved change
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex place-items-center gap-1 h-[26px]">
|
||||
<label class="immich-form-label text-sm" for={title}>
|
||||
{title}
|
||||
</label>
|
||||
{#if isEdited}
|
||||
<div
|
||||
transition:fly={{ x: 10, duration: 200, easing: quintOut }}
|
||||
class="bg-orange-100 px-2 rounded-full text-orange-900 text-[10px]"
|
||||
>
|
||||
Unsaved change
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
|
||||
</div>
|
||||
<p class="text-sm dark:text-immich-dark-fg">{subtitle}</p>
|
||||
</div>
|
||||
|
||||
<label class="relative inline-block flex-none w-[36px] h-[10px]">
|
||||
<input
|
||||
class="opacity-0 w-0 h-0 disabled::cursor-not-allowed"
|
||||
type="checkbox"
|
||||
bind:checked
|
||||
on:click
|
||||
{disabled}
|
||||
/>
|
||||
<label class="relative inline-block flex-none w-[36px] h-[10px]">
|
||||
<input class="opacity-0 w-0 h-0 disabled::cursor-not-allowed" type="checkbox" bind:checked on:click {disabled} />
|
||||
|
||||
{#if disabled}
|
||||
<span class="slider-disable" />
|
||||
{:else}
|
||||
<span class="slider" />
|
||||
{/if}
|
||||
</label>
|
||||
{#if disabled}
|
||||
<span class="slider-disable" />
|
||||
{:else}
|
||||
<span class="slider" />
|
||||
{/if}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.slider,
|
||||
.slider-disable {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: 0.4s;
|
||||
transition: 0.4s;
|
||||
border-radius: 34px;
|
||||
}
|
||||
.slider,
|
||||
.slider-disable {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #ccc;
|
||||
-webkit-transition: 0.4s;
|
||||
transition: 0.4s;
|
||||
border-radius: 34px;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
input:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.slider:before,
|
||||
.slider-disable:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: -4px;
|
||||
background-color: gray;
|
||||
-webkit-transition: 0.4s;
|
||||
transition: 0.4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.slider:before,
|
||||
.slider-disable:before {
|
||||
position: absolute;
|
||||
content: '';
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: -4px;
|
||||
background-color: gray;
|
||||
-webkit-transition: 0.4s;
|
||||
transition: 0.4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
input:checked + .slider-disable {
|
||||
background-color: gray;
|
||||
}
|
||||
input:checked + .slider-disable {
|
||||
background-color: gray;
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #adcbfa;
|
||||
}
|
||||
input:checked + .slider {
|
||||
background-color: #adcbfa;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(18px);
|
||||
-ms-transform: translateX(18px);
|
||||
transform: translateX(18px);
|
||||
background-color: #4250af;
|
||||
}
|
||||
input:checked + .slider:before {
|
||||
-webkit-transform: translateX(18px);
|
||||
-ms-transform: translateX(18px);
|
||||
transform: translateX(18px);
|
||||
background-color: #4250af;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,241 +1,224 @@
|
|||
<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 { isEqual } from 'lodash-es';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
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 { isEqual } from 'lodash-es';
|
||||
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;
|
||||
export let storageConfig: SystemConfigStorageTemplateDto;
|
||||
export let user: UserResponseDto;
|
||||
|
||||
let savedConfig: SystemConfigStorageTemplateDto;
|
||||
let defaultConfig: SystemConfigStorageTemplateDto;
|
||||
let templateOptions: SystemConfigTemplateStorageOptionDto;
|
||||
let selectedPreset = '';
|
||||
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)
|
||||
]);
|
||||
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 = savedConfig.template;
|
||||
}
|
||||
selectedPreset = savedConfig.template;
|
||||
}
|
||||
|
||||
const getSupportDateTimeFormat = async () => {
|
||||
const { data } = await api.systemConfigApi.getStorageTemplateOptions();
|
||||
return data;
|
||||
};
|
||||
const getSupportDateTimeFormat = async () => {
|
||||
const { data } = await api.systemConfigApi.getStorageTemplateOptions();
|
||||
return data;
|
||||
};
|
||||
|
||||
$: parsedTemplate = () => {
|
||||
try {
|
||||
return renderTemplate(storageConfig.template);
|
||||
} catch (error) {
|
||||
return 'error';
|
||||
}
|
||||
};
|
||||
$: parsedTemplate = () => {
|
||||
try {
|
||||
return renderTemplate(storageConfig.template);
|
||||
} catch (error) {
|
||||
return 'error';
|
||||
}
|
||||
};
|
||||
|
||||
const renderTemplate = (templateString: string) => {
|
||||
const template = handlebar.compile(templateString, {
|
||||
knownHelpers: undefined
|
||||
});
|
||||
const renderTemplate = (templateString: string) => {
|
||||
const template = handlebar.compile(templateString, {
|
||||
knownHelpers: undefined,
|
||||
});
|
||||
|
||||
const substitutions: Record<string, string> = {
|
||||
filename: 'IMAGE_56437',
|
||||
ext: 'jpg',
|
||||
filetype: 'IMG',
|
||||
filetypefull: 'IMAGE'
|
||||
};
|
||||
const substitutions: Record<string, string> = {
|
||||
filename: 'IMAGE_56437',
|
||||
ext: 'jpg',
|
||||
filetype: 'IMG',
|
||||
filetypefull: 'IMAGE',
|
||||
};
|
||||
|
||||
const dt = luxon.DateTime.fromISO(new Date('2022-09-04T20:03:05.250').toISOString());
|
||||
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
|
||||
];
|
||||
const dateTokens = [
|
||||
...templateOptions.yearOptions,
|
||||
...templateOptions.monthOptions,
|
||||
...templateOptions.dayOptions,
|
||||
...templateOptions.hourOptions,
|
||||
...templateOptions.minuteOptions,
|
||||
...templateOptions.secondOptions,
|
||||
];
|
||||
|
||||
for (const token of dateTokens) {
|
||||
substitutions[token] = dt.toFormat(token);
|
||||
}
|
||||
for (const token of dateTokens) {
|
||||
substitutions[token] = dt.toFormat(token);
|
||||
}
|
||||
|
||||
return template(substitutions);
|
||||
};
|
||||
return template(substitutions);
|
||||
};
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
storageConfig.template = resetConfig.storageTemplate.template;
|
||||
savedConfig.template = resetConfig.storageTemplate.template;
|
||||
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
|
||||
});
|
||||
}
|
||||
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();
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: currentConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
const result = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...currentConfig,
|
||||
storageTemplate: storageConfig
|
||||
}
|
||||
});
|
||||
const result = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...currentConfig,
|
||||
storageTemplate: storageConfig,
|
||||
},
|
||||
});
|
||||
|
||||
storageConfig.template = result.data.storageTemplate.template;
|
||||
savedConfig.template = result.data.storageTemplate.template;
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
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();
|
||||
async function resetToDefault() {
|
||||
const { data: defaultConfig } = await api.systemConfigApi.getDefaults();
|
||||
|
||||
storageConfig.template = defaultConfig.storageTemplate.template;
|
||||
storageConfig.template = defaultConfig.storageTemplate.template;
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset storage template to default',
|
||||
type: NotificationType.Info
|
||||
});
|
||||
}
|
||||
notificationController.show({
|
||||
message: 'Reset storage template to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
const handlePresetSelection = () => {
|
||||
storageConfig.template = selectedPreset;
|
||||
};
|
||||
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>
|
||||
{#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">
|
||||
{#await getSupportDateTimeFormat()}
|
||||
<LoadingSpinner />
|
||||
{:then options}
|
||||
<div transition:fade={{ duration: 200 }}>
|
||||
<SupportedDatetimePanel {options} />
|
||||
</div>
|
||||
{/await}
|
||||
</section>
|
||||
|
||||
<section class="support-date">
|
||||
<SupportedVariablesPanel />
|
||||
</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="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>
|
||||
<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">
|
||||
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">
|
||||
<code>{user.storageLabel || user.id}</code> is the user's Storage Label
|
||||
</p>
|
||||
<p class="text-xs">
|
||||
<code>{user.storageLabel || user.id}</code> is the user's Storage Label
|
||||
</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.storageLabel || user.id}</span
|
||||
>/{parsedTemplate()}.jpg
|
||||
</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.storageLabel || user.id}</span
|
||||
>/{parsedTemplate()}.jpg
|
||||
</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)}
|
||||
/>
|
||||
<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={'.jpg'}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-0">
|
||||
<SettingInputField label="EXTENSION" inputType={SettingInputFieldType.TEXT} value={'.jpg'} disabled />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="migration-info" class="text-sm mt-4">
|
||||
<p>
|
||||
Template changes will only apply to new assets. To retroactively apply the template to
|
||||
previously uploaded assets, run the <a
|
||||
href="/admin/jobs-status"
|
||||
class="text-immich-primary dark:text-immich-dark-primary">Storage Migration Job</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div id="migration-info" class="text-sm mt-4">
|
||||
<p>
|
||||
Template changes will only apply to new assets. To retroactively apply the template to previously uploaded
|
||||
assets, run the <a href="/admin/jobs-status" class="text-immich-primary dark:text-immich-dark-primary"
|
||||
>Storage Migration Job</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -1,78 +1,76 @@
|
|||
<script lang="ts">
|
||||
import type { SystemConfigTemplateStorageOptionDto } from '@api';
|
||||
import * as luxon from 'luxon';
|
||||
import type { SystemConfigTemplateStorageOptionDto } from '@api';
|
||||
import * as luxon from 'luxon';
|
||||
|
||||
export let options: SystemConfigTemplateStorageOptionDto;
|
||||
export let options: SystemConfigTemplateStorageOptionDto;
|
||||
|
||||
const getLuxonExample = (format: string) => {
|
||||
return luxon.DateTime.fromISO(new Date('2022-09-04T20:03:05.250').toISOString()).toFormat(
|
||||
format
|
||||
);
|
||||
};
|
||||
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>
|
||||
<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 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">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">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">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">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>
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1,29 +1,29 @@
|
|||
<div class="text-xs mt-4">
|
||||
<h4>OTHER VARIABLES</h4>
|
||||
<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 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>
|
||||
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">FILE EXTENSION</p>
|
||||
<ul>
|
||||
<li>{`{{ext}}`}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">FILE TYPE</p>
|
||||
<ul>
|
||||
<li>{`{{filetype}}`} - VID or IMG</li>
|
||||
<li>{`{{filetypefull}}`} - VIDEO or IMAGE</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-immich-primary font-medium dark:text-immich-dark-primary">FILE TYPE</p>
|
||||
<ul>
|
||||
<li>{`{{filetype}}`} - VID or IMG</li>
|
||||
<li>{`{{filetypefull}}`} - VIDEO or IMAGE</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue