Compare commits
11 commits
main
...
refactor/s
Author | SHA1 | Date | |
---|---|---|---|
|
1cb58728b2 | ||
|
406754195b | ||
|
1d544c8c4d | ||
|
7226d12d5d | ||
|
8b7a4f2169 | ||
|
77fb3e9ce2 | ||
|
4e98b1001b | ||
|
6e2ba7acab | ||
|
3674e5b858 | ||
|
4b1e1f6e83 | ||
|
0e4b00d07a |
9 changed files with 846 additions and 980 deletions
|
@ -3,79 +3,35 @@
|
||||||
notificationController,
|
notificationController,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import {
|
import { AudioCodec, SystemConfigFFmpegDto, ToneMapping, TranscodeHWAccel, TranscodePolicy, VideoCodec } from '@api';
|
||||||
api,
|
|
||||||
AudioCodec,
|
|
||||||
SystemConfigFFmpegDto,
|
|
||||||
ToneMapping,
|
|
||||||
TranscodeHWAccel,
|
|
||||||
TranscodePolicy,
|
|
||||||
VideoCodec,
|
|
||||||
} from '@api';
|
|
||||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||||
import SettingSelect from '../setting-select.svelte';
|
import SettingSelect from '../setting-select.svelte';
|
||||||
import SettingSwitch from '../setting-switch.svelte';
|
import SettingSwitch from '../setting-switch.svelte';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
export let ffmpegConfig: SystemConfigFFmpegDto; // this is the config that is being edited
|
export let ffmpegConfig: SystemConfigFFmpegDto; // this is the config that is being edited
|
||||||
|
export let ffmpegDefault: SystemConfigFFmpegDto;
|
||||||
|
export let savedConfig: SystemConfigFFmpegDto;
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
|
|
||||||
let savedConfig: SystemConfigFFmpegDto;
|
const dispatch = createEventDispatcher<{
|
||||||
let defaultConfig: SystemConfigFFmpegDto;
|
save: 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 saveSetting() {
|
|
||||||
try {
|
|
||||||
const { data: configs } = await api.systemConfigApi.getConfig();
|
|
||||||
|
|
||||||
const result = await api.systemConfigApi.updateConfig({
|
|
||||||
systemConfigDto: {
|
|
||||||
...configs,
|
|
||||||
ffmpeg: ffmpegConfig,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reset() {
|
async function reset() {
|
||||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
ffmpegConfig = { ...savedConfig };
|
||||||
|
|
||||||
ffmpegConfig = { ...resetConfig.ffmpeg };
|
|
||||||
savedConfig = { ...resetConfig.ffmpeg };
|
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset FFmpeg settings to the recent saved settings',
|
message: 'Reset FFmpeg settings to the last saved settings',
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetToDefault() {
|
async function resetToDefault() {
|
||||||
const { data: configs } = await api.systemConfigApi.getDefaults();
|
ffmpegConfig = { ...ffmpegDefault };
|
||||||
|
|
||||||
ffmpegConfig = { ...configs.ffmpeg };
|
|
||||||
defaultConfig = { ...configs.ffmpeg };
|
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset FFmpeg settings to default',
|
message: 'Reset FFmpeg settings to default',
|
||||||
|
@ -85,197 +41,195 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{#await getConfigs() then}
|
<div in:fade={{ duration: 300 }}>
|
||||||
<div in:fade={{ duration: 500 }}>
|
<form autocomplete="off" on:submit|preventDefault>
|
||||||
<form autocomplete="off" on:submit|preventDefault>
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<SettingInputField
|
||||||
<SettingInputField
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
{disabled}
|
||||||
{disabled}
|
label="CONSTANT RATE FACTOR (-crf)"
|
||||||
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."
|
||||||
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}
|
||||||
bind:value={ffmpegConfig.crf}
|
required={true}
|
||||||
required={true}
|
isEdited={!(ffmpegConfig.crf == savedConfig.crf)}
|
||||||
isEdited={!(ffmpegConfig.crf == savedConfig.crf)}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="PRESET (-preset)"
|
label="PRESET (-preset)"
|
||||||
{disabled}
|
{disabled}
|
||||||
desc="Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above `faster`."
|
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}
|
bind:value={ffmpegConfig.preset}
|
||||||
name="preset"
|
name="preset"
|
||||||
options={[
|
options={[
|
||||||
{ value: 'ultrafast', text: 'ultrafast' },
|
{ value: 'ultrafast', text: 'ultrafast' },
|
||||||
{ value: 'superfast', text: 'superfast' },
|
{ value: 'superfast', text: 'superfast' },
|
||||||
{ value: 'veryfast', text: 'veryfast' },
|
{ value: 'veryfast', text: 'veryfast' },
|
||||||
{ value: 'faster', text: 'faster' },
|
{ value: 'faster', text: 'faster' },
|
||||||
{ value: 'fast', text: 'fast' },
|
{ value: 'fast', text: 'fast' },
|
||||||
{ value: 'medium', text: 'medium' },
|
{ value: 'medium', text: 'medium' },
|
||||||
{ value: 'slow', text: 'slow' },
|
{ value: 'slow', text: 'slow' },
|
||||||
{ value: 'slower', text: 'slower' },
|
{ value: 'slower', text: 'slower' },
|
||||||
{ value: 'veryslow', text: 'veryslow' },
|
{ value: 'veryslow', text: 'veryslow' },
|
||||||
]}
|
]}
|
||||||
isEdited={!(ffmpegConfig.preset == savedConfig.preset)}
|
isEdited={!(ffmpegConfig.preset == savedConfig.preset)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="AUDIO CODEC"
|
label="AUDIO CODEC"
|
||||||
{disabled}
|
{disabled}
|
||||||
desc="Opus is the highest quality option, but has lower compatibility with old devices or software."
|
desc="Opus is the highest quality option, but has lower compatibility with old devices or software."
|
||||||
bind:value={ffmpegConfig.targetAudioCodec}
|
bind:value={ffmpegConfig.targetAudioCodec}
|
||||||
options={[
|
options={[
|
||||||
{ value: AudioCodec.Aac, text: 'aac' },
|
{ value: AudioCodec.Aac, text: 'aac' },
|
||||||
{ value: AudioCodec.Mp3, text: 'mp3' },
|
{ value: AudioCodec.Mp3, text: 'mp3' },
|
||||||
{ value: AudioCodec.Opus, text: 'opus' },
|
{ value: AudioCodec.Opus, text: 'opus' },
|
||||||
]}
|
]}
|
||||||
name="acodec"
|
name="acodec"
|
||||||
isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)}
|
isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="VIDEO CODEC"
|
label="VIDEO CODEC"
|
||||||
{disabled}
|
{disabled}
|
||||||
desc="VP9 has high efficiency and web compatibility, but takes longer to transcode. HEVC performs similarly, but has lower web compatibility. H.264 is widely compatible and quick to transcode, but produces much larger files."
|
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}
|
bind:value={ffmpegConfig.targetVideoCodec}
|
||||||
options={[
|
options={[
|
||||||
{ value: VideoCodec.H264, text: 'h264' },
|
{ value: VideoCodec.H264, text: 'h264' },
|
||||||
{ value: VideoCodec.Hevc, text: 'hevc' },
|
{ value: VideoCodec.Hevc, text: 'hevc' },
|
||||||
{ value: VideoCodec.Vp9, text: 'vp9' },
|
{ value: VideoCodec.Vp9, text: 'vp9' },
|
||||||
]}
|
]}
|
||||||
name="vcodec"
|
name="vcodec"
|
||||||
isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)}
|
isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="TARGET RESOLUTION"
|
label="TARGET RESOLUTION"
|
||||||
{disabled}
|
{disabled}
|
||||||
desc="Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
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}
|
bind:value={ffmpegConfig.targetResolution}
|
||||||
options={[
|
options={[
|
||||||
{ value: '2160', text: '4k' },
|
{ value: '2160', text: '4k' },
|
||||||
{ value: '1440', text: '1440p' },
|
{ value: '1440', text: '1440p' },
|
||||||
{ value: '1080', text: '1080p' },
|
{ value: '1080', text: '1080p' },
|
||||||
{ value: '720', text: '720p' },
|
{ value: '720', text: '720p' },
|
||||||
{ value: '480', text: '480p' },
|
{ value: '480', text: '480p' },
|
||||||
{ value: 'original', text: 'original' },
|
{ value: 'original', text: 'original' },
|
||||||
]}
|
]}
|
||||||
name="resolution"
|
name="resolution"
|
||||||
isEdited={!(ffmpegConfig.targetResolution == savedConfig.targetResolution)}
|
isEdited={!(ffmpegConfig.targetResolution == savedConfig.targetResolution)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
{disabled}
|
{disabled}
|
||||||
label="MAX BITRATE"
|
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."
|
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}
|
bind:value={ffmpegConfig.maxBitrate}
|
||||||
isEdited={!(ffmpegConfig.maxBitrate == savedConfig.maxBitrate)}
|
isEdited={!(ffmpegConfig.maxBitrate == savedConfig.maxBitrate)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
{disabled}
|
{disabled}
|
||||||
label="THREADS"
|
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."
|
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}
|
bind:value={ffmpegConfig.threads}
|
||||||
isEdited={!(ffmpegConfig.threads == savedConfig.threads)}
|
isEdited={!(ffmpegConfig.threads == savedConfig.threads)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="TRANSCODE POLICY"
|
label="TRANSCODE POLICY"
|
||||||
{disabled}
|
{disabled}
|
||||||
desc="Policy for when a video should be transcoded."
|
desc="Policy for when a video should be transcoded."
|
||||||
bind:value={ffmpegConfig.transcode}
|
bind:value={ffmpegConfig.transcode}
|
||||||
name="transcode"
|
name="transcode"
|
||||||
options={[
|
options={[
|
||||||
{ value: TranscodePolicy.All, text: 'All videos' },
|
{ value: TranscodePolicy.All, text: 'All videos' },
|
||||||
{
|
{
|
||||||
value: TranscodePolicy.Optimal,
|
value: TranscodePolicy.Optimal,
|
||||||
text: 'Videos higher than target resolution or not in the desired format',
|
text: 'Videos higher than target resolution or not in the desired format',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: TranscodePolicy.Required,
|
value: TranscodePolicy.Required,
|
||||||
text: 'Only videos not in the desired format',
|
text: 'Only videos not in the desired format',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: TranscodePolicy.Disabled,
|
value: TranscodePolicy.Disabled,
|
||||||
text: "Don't transcode any videos, may break playback on some clients",
|
text: "Don't transcode any videos, may break playback on some clients",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
isEdited={!(ffmpegConfig.transcode == savedConfig.transcode)}
|
isEdited={!(ffmpegConfig.transcode == savedConfig.transcode)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="HARDWARE ACCELERATION"
|
label="HARDWARE ACCELERATION"
|
||||||
{disabled}
|
{disabled}
|
||||||
desc="Experimental. Much faster, but will have lower quality at the same bitrate. This setting is 'best effort': it will fallback to software transcoding on failure. VP9 may or may not work depending on your hardware."
|
desc="Experimental. Much faster, but will have lower quality at the same bitrate. This setting is 'best effort': it will fallback to software transcoding on failure. VP9 may or may not work depending on your hardware."
|
||||||
bind:value={ffmpegConfig.accel}
|
bind:value={ffmpegConfig.accel}
|
||||||
name="accel"
|
name="accel"
|
||||||
options={[
|
options={[
|
||||||
{ value: TranscodeHWAccel.Nvenc, text: 'NVENC (requires NVIDIA GPU)' },
|
{ value: TranscodeHWAccel.Nvenc, text: 'NVENC (requires NVIDIA GPU)' },
|
||||||
{
|
{
|
||||||
value: TranscodeHWAccel.Qsv,
|
value: TranscodeHWAccel.Qsv,
|
||||||
text: 'Quick Sync (requires 7th gen Intel CPU or later)',
|
text: 'Quick Sync (requires 7th gen Intel CPU or later)',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: TranscodeHWAccel.Vaapi,
|
value: TranscodeHWAccel.Vaapi,
|
||||||
text: 'VAAPI',
|
text: 'VAAPI',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: TranscodeHWAccel.Disabled,
|
value: TranscodeHWAccel.Disabled,
|
||||||
text: 'Disabled',
|
text: 'Disabled',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
isEdited={!(ffmpegConfig.accel == savedConfig.accel)}
|
isEdited={!(ffmpegConfig.accel == savedConfig.accel)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="TONE-MAPPING"
|
label="TONE-MAPPING"
|
||||||
{disabled}
|
{disabled}
|
||||||
desc="Attempts to preserve the appearance of HDR videos when converted to SDR. Each algorithm makes different tradeoffs for color, detail and brightness. Hable preserves detail, Mobius preserves color, and Reinhard preserves brightness."
|
desc="Attempts to preserve the appearance of HDR videos when converted to SDR. Each algorithm makes different tradeoffs for color, detail and brightness. Hable preserves detail, Mobius preserves color, and Reinhard preserves brightness."
|
||||||
bind:value={ffmpegConfig.tonemap}
|
bind:value={ffmpegConfig.tonemap}
|
||||||
name="tonemap"
|
name="tonemap"
|
||||||
options={[
|
options={[
|
||||||
{
|
{
|
||||||
value: ToneMapping.Hable,
|
value: ToneMapping.Hable,
|
||||||
text: 'Hable',
|
text: 'Hable',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: ToneMapping.Mobius,
|
value: ToneMapping.Mobius,
|
||||||
text: 'Mobius',
|
text: 'Mobius',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: ToneMapping.Reinhard,
|
value: ToneMapping.Reinhard,
|
||||||
text: 'Reinhard',
|
text: 'Reinhard',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: ToneMapping.Disabled,
|
value: ToneMapping.Disabled,
|
||||||
text: 'Disabled',
|
text: 'Disabled',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
isEdited={!(ffmpegConfig.tonemap == savedConfig.tonemap)}
|
isEdited={!(ffmpegConfig.tonemap == savedConfig.tonemap)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="TWO-PASS ENCODING"
|
title="TWO-PASS ENCODING"
|
||||||
{disabled}
|
{disabled}
|
||||||
subtitle="Transcode in two passes to produce better encoded videos. When max bitrate is enabled (required for it to work with H.264 and HEVC), this mode uses a bitrate range based on the max bitrate and ignores CRF. For VP9, CRF can be used if max bitrate is disabled."
|
subtitle="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}
|
bind:checked={ffmpegConfig.twoPass}
|
||||||
isEdited={!(ffmpegConfig.twoPass === savedConfig.twoPass)}
|
isEdited={!(ffmpegConfig.twoPass === savedConfig.twoPass)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<SettingButtonsRow
|
<SettingButtonsRow
|
||||||
on:reset={reset}
|
on:reset={reset}
|
||||||
on:save={saveSetting}
|
on:save={() => dispatch('save', ffmpegConfig)}
|
||||||
on:reset-to-default={resetToDefault}
|
on:reset-to-default={resetToDefault}
|
||||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
showResetToDefault={!isEqual(ffmpegConfig, ffmpegDefault)}
|
||||||
{disabled}
|
{disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -6,51 +6,24 @@
|
||||||
import { api, JobName, SystemConfigJobDto } from '@api';
|
import { api, JobName, SystemConfigJobDto } from '@api';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { handleError } from '../../../../utils/handle-error';
|
|
||||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
save: SystemConfigJobDto;
|
||||||
|
}>();
|
||||||
|
|
||||||
export let jobConfig: SystemConfigJobDto; // this is the config that is being edited
|
export let jobConfig: SystemConfigJobDto; // this is the config that is being edited
|
||||||
|
export let jobDefault: SystemConfigJobDto;
|
||||||
|
export let savedConfig: SystemConfigJobDto;
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
|
|
||||||
let savedConfig: SystemConfigJobDto;
|
|
||||||
let defaultConfig: SystemConfigJobDto;
|
|
||||||
|
|
||||||
const ignoredJobs = [JobName.BackgroundTask, JobName.Search] as JobName[];
|
const ignoredJobs = [JobName.BackgroundTask, JobName.Search] as JobName[];
|
||||||
const jobNames = Object.values(JobName).filter((jobName) => !ignoredJobs.includes(jobName 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 saveSetting() {
|
|
||||||
try {
|
|
||||||
const { data: configs } = await api.systemConfigApi.getConfig();
|
|
||||||
|
|
||||||
const result = await api.systemConfigApi.updateConfig({
|
|
||||||
systemConfigDto: {
|
|
||||||
...configs,
|
|
||||||
job: jobConfig,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reset() {
|
async function reset() {
|
||||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
jobConfig = { ...savedConfig };
|
||||||
|
|
||||||
jobConfig = { ...resetConfig.job };
|
|
||||||
savedConfig = { ...resetConfig.job };
|
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset Job settings to the recent saved settings',
|
message: 'Reset Job settings to the recent saved settings',
|
||||||
|
@ -59,10 +32,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetToDefault() {
|
async function resetToDefault() {
|
||||||
const { data: configs } = await api.systemConfigApi.getDefaults();
|
jobConfig = { ...jobDefault };
|
||||||
|
|
||||||
jobConfig = { ...configs.job };
|
|
||||||
defaultConfig = { ...configs.job };
|
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset Job settings to default',
|
message: 'Reset Job settings to default',
|
||||||
|
@ -72,33 +42,30 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{#await getConfigs() then}
|
<div in:fade={{ duration: 300 }}>
|
||||||
<div in:fade={{ duration: 500 }}>
|
<form autocomplete="off" on:submit|preventDefault>
|
||||||
<form autocomplete="off" on:submit|preventDefault>
|
{#each jobNames as jobName}
|
||||||
{#each jobNames as jobName}
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<SettingInputField
|
||||||
<SettingInputField
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
|
||||||
{disabled}
|
|
||||||
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)}
|
|
||||||
{disabled}
|
{disabled}
|
||||||
|
label="{api.getJobName(jobName)} Concurrency"
|
||||||
|
desc=""
|
||||||
|
bind:value={jobConfig[jobName].concurrency}
|
||||||
|
required={true}
|
||||||
|
isEdited={!(jobConfig[jobName].concurrency == savedConfig[jobName].concurrency)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
{/each}
|
||||||
</div>
|
|
||||||
{/await}
|
<div class="ml-4">
|
||||||
|
<SettingButtonsRow
|
||||||
|
on:reset={reset}
|
||||||
|
on:save={() => dispatch('save', jobConfig)}
|
||||||
|
on:reset-to-default={resetToDefault}
|
||||||
|
showResetToDefault={!isEqual(jobConfig, jobDefault)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,210 +3,186 @@
|
||||||
notificationController,
|
notificationController,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
|
||||||
import { api, SystemConfigMachineLearningDto } from '@api';
|
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||||
import SettingSwitch from '../setting-switch.svelte';
|
import SettingSwitch from '../setting-switch.svelte';
|
||||||
|
import type { SystemConfigMachineLearningDto } from '@api';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
import SettingAccordion from '../setting-accordion.svelte';
|
import SettingAccordion from '../setting-accordion.svelte';
|
||||||
import SettingSelect from '../setting-select.svelte';
|
import SettingSelect from '../setting-select.svelte';
|
||||||
|
|
||||||
export let machineLearningConfig: SystemConfigMachineLearningDto; // this is the config that is being edited
|
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
|
|
||||||
let savedConfig: SystemConfigMachineLearningDto;
|
export let machineLearningConfig: SystemConfigMachineLearningDto; // this is the config that is being edited
|
||||||
let defaultConfig: SystemConfigMachineLearningDto;
|
export let machineLearningDefault: SystemConfigMachineLearningDto;
|
||||||
|
export let savedConfig: SystemConfigMachineLearningDto;
|
||||||
|
|
||||||
async function refreshConfig() {
|
const dispatch = createEventDispatcher<{
|
||||||
[savedConfig, defaultConfig] = await Promise.all([
|
save: SystemConfigMachineLearningDto;
|
||||||
api.systemConfigApi.getConfig().then((res) => res.data.machineLearning),
|
}>();
|
||||||
api.systemConfigApi.getDefaults().then((res) => res.data.machineLearning),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reset() {
|
async function reset() {
|
||||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
machineLearningConfig = { ...savedConfig };
|
||||||
machineLearningConfig = { ...resetConfig.machineLearning };
|
|
||||||
savedConfig = { ...resetConfig.machineLearning };
|
|
||||||
notificationController.show({ message: 'Reset to the last saved settings', type: NotificationType.Info });
|
notificationController.show({ message: 'Reset to the last saved settings', type: NotificationType.Info });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSetting() {
|
|
||||||
try {
|
|
||||||
const { data: current } = await api.systemConfigApi.getConfig();
|
|
||||||
const result = await api.systemConfigApi.updateConfig({
|
|
||||||
systemConfigDto: { ...current, machineLearning: machineLearningConfig },
|
|
||||||
});
|
|
||||||
|
|
||||||
machineLearningConfig = { ...result.data.machineLearning };
|
|
||||||
savedConfig = { ...result.data.machineLearning };
|
|
||||||
|
|
||||||
notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, 'Unable to save settings');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resetToDefault() {
|
async function resetToDefault() {
|
||||||
machineLearningConfig = { ...defaultConfig };
|
machineLearningConfig = { ...machineLearningDefault };
|
||||||
notificationController.show({ message: 'Reset settings to defaults', type: NotificationType.Info });
|
notificationController.show({ message: 'Reset settings to defaults', type: NotificationType.Info });
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
{#await refreshConfig() then}
|
<div in:fade={{ duration: 500 }}>
|
||||||
<div in:fade={{ duration: 500 }}>
|
<form autocomplete="off" on:submit|preventDefault class="mx-4 mt-4">
|
||||||
<form autocomplete="off" on:submit|preventDefault class="mx-4 mt-4">
|
<div class="flex flex-col gap-4">
|
||||||
<div class="flex flex-col gap-4">
|
<SettingSwitch
|
||||||
|
title="ENABLED"
|
||||||
|
subtitle="If disabled, all ML features will be disabled regardless of the below settings."
|
||||||
|
{disabled}
|
||||||
|
bind:checked={machineLearningConfig.enabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<SettingInputField
|
||||||
|
inputType={SettingInputFieldType.TEXT}
|
||||||
|
label="URL"
|
||||||
|
desc="URL of the machine learning server"
|
||||||
|
bind:value={machineLearningConfig.url}
|
||||||
|
required={true}
|
||||||
|
disabled={disabled || !machineLearningConfig.enabled}
|
||||||
|
isEdited={machineLearningConfig.url !== savedConfig.url}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SettingAccordion title="Image Tagging" subtitle="Tag and classify images with object labels">
|
||||||
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="ENABLED"
|
title="ENABLED"
|
||||||
subtitle="If disabled, all ML features will be disabled regardless of the below settings."
|
subtitle="If disabled, images will not be tagged. This affects the Things section in the Explore page as well as 'm:' searches."
|
||||||
{disabled}
|
bind:checked={machineLearningConfig.classification.enabled}
|
||||||
bind:checked={machineLearningConfig.enabled}
|
disabled={disabled || !machineLearningConfig.enabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="URL"
|
label="IMAGE CLASSIFICATION MODEL"
|
||||||
desc="URL of the machine learning server"
|
bind:value={machineLearningConfig.classification.modelName}
|
||||||
bind:value={machineLearningConfig.url}
|
|
||||||
required={true}
|
required={true}
|
||||||
disabled={disabled || !machineLearningConfig.enabled}
|
disabled={disabled || !machineLearningConfig.enabled || !machineLearningConfig.classification.enabled}
|
||||||
isEdited={machineLearningConfig.url !== savedConfig.url}
|
isEdited={machineLearningConfig.classification.modelName !== savedConfig.classification.modelName}
|
||||||
|
>
|
||||||
|
<p slot="desc" class="immich-form-label pb-2 text-sm">
|
||||||
|
The name of an image classification model listed <a
|
||||||
|
href="https://huggingface.co/models?pipeline_tag=image-classification&sort=trending"><u>here</u></a
|
||||||
|
>. It must be tagged with the 'Image Classification' task and must support ONNX conversion.
|
||||||
|
</p>
|
||||||
|
</SettingInputField>
|
||||||
|
|
||||||
|
<SettingInputField
|
||||||
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
|
label="IMAGE CLASSIFICATION THRESHOLD"
|
||||||
|
desc="Minimum confidence score to add a particular object tag. Lower values will add more tags to images, but may result in more false positives. Will not have any effect until the Tag Objects job is re-run."
|
||||||
|
bind:value={machineLearningConfig.classification.minScore}
|
||||||
|
step="0.1"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
disabled={disabled || !machineLearningConfig.enabled || !machineLearningConfig.classification.enabled}
|
||||||
|
isEdited={machineLearningConfig.classification.minScore !== savedConfig.classification.minScore}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</SettingAccordion>
|
||||||
|
|
||||||
<SettingAccordion title="Image Tagging" subtitle="Tag and classify images with object labels">
|
<SettingAccordion title="Smart Search" subtitle="Search for images semantically using CLIP embeddings">
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<SettingSwitch
|
<SettingSwitch
|
||||||
title="ENABLED"
|
title="ENABLED"
|
||||||
subtitle="If disabled, images will not be tagged. This affects the Things section in the Explore page as well as 'm:' searches."
|
subtitle="If disabled, images will not be encoded for smart search."
|
||||||
bind:checked={machineLearningConfig.classification.enabled}
|
bind:checked={machineLearningConfig.clip.enabled}
|
||||||
disabled={disabled || !machineLearningConfig.enabled}
|
disabled={disabled || !machineLearningConfig.enabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="IMAGE CLASSIFICATION MODEL"
|
label="CLIP MODEL"
|
||||||
bind:value={machineLearningConfig.classification.modelName}
|
bind:value={machineLearningConfig.clip.modelName}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={disabled || !machineLearningConfig.enabled || !machineLearningConfig.classification.enabled}
|
disabled={disabled || !machineLearningConfig.enabled || !machineLearningConfig.clip.enabled}
|
||||||
isEdited={machineLearningConfig.classification.modelName !== savedConfig.classification.modelName}
|
isEdited={machineLearningConfig.clip.modelName !== savedConfig.clip.modelName}
|
||||||
>
|
>
|
||||||
<p slot="desc" class="immich-form-label pb-2 text-sm">
|
<p slot="desc" class="immich-form-label pb-2 text-sm">
|
||||||
The name of an image classification model listed <a
|
The name of a CLIP model listed <a
|
||||||
href="https://huggingface.co/models?pipeline_tag=image-classification&sort=trending"><u>here</u></a
|
href="https://clip-as-service.jina.ai/user-guides/benchmark/#size-and-efficiency"><u>here</u></a
|
||||||
>. It must be tagged with the 'Image Classification' task and must support ONNX conversion.
|
>. Note that you must re-run the 'Encode CLIP' job for all images upon changing a model.
|
||||||
</p>
|
</p>
|
||||||
</SettingInputField>
|
</SettingInputField>
|
||||||
|
</div>
|
||||||
|
</SettingAccordion>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingAccordion title="Facial Recognition" subtitle="Detect, recognize and group faces in images">
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
label="IMAGE CLASSIFICATION THRESHOLD"
|
<SettingSwitch
|
||||||
desc="Minimum confidence score to add a particular object tag. Lower values will add more tags to images, but may result in more false positives. Will not have any effect until the Tag Objects job is re-run."
|
title="ENABLED"
|
||||||
bind:value={machineLearningConfig.classification.minScore}
|
subtitle="If disabled, images will not be encoded for facial recognition and will not populate the People section in the Explore page."
|
||||||
step="0.1"
|
bind:checked={machineLearningConfig.facialRecognition.enabled}
|
||||||
min="0"
|
disabled={disabled || !machineLearningConfig.enabled}
|
||||||
max="1"
|
/>
|
||||||
disabled={disabled || !machineLearningConfig.enabled || !machineLearningConfig.classification.enabled}
|
|
||||||
isEdited={machineLearningConfig.classification.minScore !== savedConfig.classification.minScore}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingAccordion>
|
|
||||||
|
|
||||||
<SettingAccordion title="Smart Search" subtitle="Search for images semantically using CLIP embeddings">
|
<hr />
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
|
||||||
<SettingSwitch
|
|
||||||
title="ENABLED"
|
|
||||||
subtitle="If disabled, images will not be encoded for smart search."
|
|
||||||
bind:checked={machineLearningConfig.clip.enabled}
|
|
||||||
disabled={disabled || !machineLearningConfig.enabled}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<hr />
|
<SettingSelect
|
||||||
|
label="FACIAL RECOGNITION MODEL"
|
||||||
|
desc="Smaller models are faster and use less memory, but perform worse. Note that you must re-run the Recognize Faces job for all images upon changing a model."
|
||||||
|
name="facial-recognition-model"
|
||||||
|
bind:value={machineLearningConfig.facialRecognition.modelName}
|
||||||
|
options={[
|
||||||
|
{ value: 'buffalo_l', text: 'buffalo_l' },
|
||||||
|
{ value: 'buffalo_s', text: 'buffalo_s' },
|
||||||
|
]}
|
||||||
|
disabled={disabled || !machineLearningConfig.enabled || !machineLearningConfig.facialRecognition.enabled}
|
||||||
|
isEdited={machineLearningConfig.facialRecognition.modelName !== savedConfig.facialRecognition.modelName}
|
||||||
|
/>
|
||||||
|
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
label="CLIP MODEL"
|
label="MIN DETECTION SCORE"
|
||||||
bind:value={machineLearningConfig.clip.modelName}
|
desc="Minimum confidence score for a face to be detected from 0-1. Lower values will detect more faces but may result in false positives."
|
||||||
required={true}
|
bind:value={machineLearningConfig.facialRecognition.minScore}
|
||||||
disabled={disabled || !machineLearningConfig.enabled || !machineLearningConfig.clip.enabled}
|
step="0.1"
|
||||||
isEdited={machineLearningConfig.clip.modelName !== savedConfig.clip.modelName}
|
min="0"
|
||||||
>
|
max="1"
|
||||||
<p slot="desc" class="immich-form-label pb-2 text-sm">
|
disabled={disabled || !machineLearningConfig.enabled || !machineLearningConfig.facialRecognition.enabled}
|
||||||
The name of a CLIP model listed <a
|
isEdited={machineLearningConfig.facialRecognition.minScore !== savedConfig.facialRecognition.minScore}
|
||||||
href="https://clip-as-service.jina.ai/user-guides/benchmark/#size-and-efficiency"><u>here</u></a
|
/>
|
||||||
>. Note that you must re-run the 'Encode CLIP' job for all images upon changing a model.
|
|
||||||
</p>
|
|
||||||
</SettingInputField>
|
|
||||||
</div>
|
|
||||||
</SettingAccordion>
|
|
||||||
|
|
||||||
<SettingAccordion title="Facial Recognition" subtitle="Detect, recognize and group faces in images">
|
<SettingInputField
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
<SettingSwitch
|
label="MAX RECOGNITION DISTANCE"
|
||||||
title="ENABLED"
|
desc="Maximum distance between two faces to be considered the same person, ranging from 0-2. Lowering this can prevent labeling two people as the same person, while raising it can prevent labeling the same person as two different people. Note that it is easier to merge two people than to split one person in two, so err on the side of a lower threshold when possible."
|
||||||
subtitle="If disabled, images will not be encoded for facial recognition and will not populate the People section in the Explore page."
|
bind:value={machineLearningConfig.facialRecognition.maxDistance}
|
||||||
bind:checked={machineLearningConfig.facialRecognition.enabled}
|
step="0.1"
|
||||||
disabled={disabled || !machineLearningConfig.enabled}
|
min="0"
|
||||||
/>
|
max="2"
|
||||||
|
disabled={disabled || !machineLearningConfig.enabled || !machineLearningConfig.facialRecognition.enabled}
|
||||||
|
isEdited={machineLearningConfig.facialRecognition.maxDistance !== savedConfig.facialRecognition.maxDistance}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingAccordion>
|
||||||
|
|
||||||
<hr />
|
<SettingButtonsRow
|
||||||
|
on:reset={reset}
|
||||||
<SettingSelect
|
on:save={() => dispatch('save', machineLearningConfig)}
|
||||||
label="FACIAL RECOGNITION MODEL"
|
on:reset-to-default={resetToDefault}
|
||||||
desc="Smaller models are faster and use less memory, but perform worse. Note that you must re-run the Recognize Faces job for all images upon changing a model."
|
showResetToDefault={!isEqual(savedConfig, machineLearningConfig)}
|
||||||
name="facial-recognition-model"
|
{disabled}
|
||||||
bind:value={machineLearningConfig.facialRecognition.modelName}
|
/>
|
||||||
options={[
|
</form>
|
||||||
{ value: 'buffalo_l', text: 'buffalo_l' },
|
</div>
|
||||||
{ value: 'buffalo_s', text: 'buffalo_s' },
|
|
||||||
]}
|
|
||||||
disabled={disabled || !machineLearningConfig.enabled || !machineLearningConfig.facialRecognition.enabled}
|
|
||||||
isEdited={machineLearningConfig.facialRecognition.modelName !== savedConfig.facialRecognition.modelName}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingInputField
|
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
|
||||||
label="MIN DETECTION SCORE"
|
|
||||||
desc="Minimum confidence score for a face to be detected from 0-1. Lower values will detect more faces but may result in false positives."
|
|
||||||
bind:value={machineLearningConfig.facialRecognition.minScore}
|
|
||||||
step="0.1"
|
|
||||||
min="0"
|
|
||||||
max="1"
|
|
||||||
disabled={disabled || !machineLearningConfig.enabled || !machineLearningConfig.facialRecognition.enabled}
|
|
||||||
isEdited={machineLearningConfig.facialRecognition.minScore !== savedConfig.facialRecognition.minScore}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingInputField
|
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
|
||||||
label="MAX RECOGNITION DISTANCE"
|
|
||||||
desc="Maximum distance between two faces to be considered the same person, ranging from 0-2. Lowering this can prevent labeling two people as the same person, while raising it can prevent labeling the same person as two different people. Note that it is easier to merge two people than to split one person in two, so err on the side of a lower threshold when possible."
|
|
||||||
bind:value={machineLearningConfig.facialRecognition.maxDistance}
|
|
||||||
step="0.1"
|
|
||||||
min="0"
|
|
||||||
max="2"
|
|
||||||
disabled={disabled || !machineLearningConfig.enabled || !machineLearningConfig.facialRecognition.enabled}
|
|
||||||
isEdited={machineLearningConfig.facialRecognition.maxDistance !==
|
|
||||||
savedConfig.facialRecognition.maxDistance}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</SettingAccordion>
|
|
||||||
|
|
||||||
<SettingButtonsRow
|
|
||||||
on:reset={reset}
|
|
||||||
on:save={saveSetting}
|
|
||||||
on:reset-to-default={resetToDefault}
|
|
||||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
|
||||||
{disabled}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{/await}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,21 +3,25 @@
|
||||||
notificationController,
|
notificationController,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import type { SystemConfigDto, SystemConfigOAuthDto } from '@api';
|
||||||
import { api, SystemConfigOAuthDto } from '@api';
|
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import ConfirmDisableLogin from '../confirm-disable-login.svelte';
|
import ConfirmDisableLogin from '../confirm-disable-login.svelte';
|
||||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||||
import SettingSwitch from '../setting-switch.svelte';
|
import SettingSwitch from '../setting-switch.svelte';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
save: SystemConfigOAuthDto;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
export let config: SystemConfigDto;
|
||||||
export let oauthConfig: SystemConfigOAuthDto;
|
export let oauthConfig: SystemConfigOAuthDto;
|
||||||
|
export let oauthDefault: SystemConfigOAuthDto;
|
||||||
|
export let savedConfig: SystemConfigOAuthDto;
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
|
|
||||||
let savedConfig: SystemConfigOAuthDto;
|
|
||||||
let defaultConfig: SystemConfigOAuthDto;
|
|
||||||
|
|
||||||
const handleToggleOverride = () => {
|
const handleToggleOverride = () => {
|
||||||
// click runs before bind
|
// click runs before bind
|
||||||
const previouslyEnabled = oauthConfig.mobileOverrideEnabled;
|
const previouslyEnabled = oauthConfig.mobileOverrideEnabled;
|
||||||
|
@ -25,19 +29,8 @@
|
||||||
oauthConfig.mobileRedirectUri = window.location.origin + '/api/oauth/mobile-redirect';
|
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 reset() {
|
async function reset() {
|
||||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
oauthConfig = { ...savedConfig };
|
||||||
|
|
||||||
oauthConfig = { ...resetConfig.oauth };
|
|
||||||
savedConfig = { ...resetConfig.oauth };
|
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset OAuth settings to the last saved settings',
|
message: 'Reset OAuth settings to the last saved settings',
|
||||||
|
@ -51,6 +44,9 @@
|
||||||
const openConfirmModal = () => {
|
const openConfirmModal = () => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
handleConfirm = (value: boolean) => {
|
handleConfirm = (value: boolean) => {
|
||||||
|
if (!value) {
|
||||||
|
oauthConfig.enabled = !oauthConfig.enabled;
|
||||||
|
}
|
||||||
isConfirmOpen = false;
|
isConfirmOpen = false;
|
||||||
resolve(value);
|
resolve(value);
|
||||||
};
|
};
|
||||||
|
@ -59,40 +55,22 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
async function saveSetting() {
|
async function saveSetting() {
|
||||||
try {
|
if (!config.passwordLogin.enabled && savedConfig.enabled && !oauthConfig.enabled) {
|
||||||
const { data: current } = await api.systemConfigApi.getConfig();
|
const confirmed = await openConfirmModal();
|
||||||
|
if (!confirmed) {
|
||||||
if (!current.passwordLogin.enabled && current.oauth.enabled && !oauthConfig.enabled) {
|
return;
|
||||||
const confirmed = await openConfirmModal();
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!oauthConfig.mobileOverrideEnabled) {
|
|
||||||
oauthConfig.mobileRedirectUri = '';
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: updated } = await api.systemConfigApi.updateConfig({
|
|
||||||
systemConfigDto: {
|
|
||||||
...current,
|
|
||||||
oauth: oauthConfig,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
oauthConfig = { ...updated.oauth };
|
|
||||||
savedConfig = { ...updated.oauth };
|
|
||||||
|
|
||||||
notificationController.show({ message: 'OAuth settings saved', type: NotificationType.Info });
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, 'Unable to save OAuth settings');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!oauthConfig.mobileOverrideEnabled) {
|
||||||
|
oauthConfig.mobileRedirectUri = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch('save', oauthConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetToDefault() {
|
async function resetToDefault() {
|
||||||
const { data: defaultConfig } = await api.systemConfigApi.getDefaults();
|
oauthConfig = { ...oauthDefault };
|
||||||
|
|
||||||
oauthConfig = { ...defaultConfig.oauth };
|
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset OAuth settings to default',
|
message: 'Reset OAuth settings to default',
|
||||||
|
@ -106,116 +84,114 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="mt-2">
|
<div class="mt-2">
|
||||||
{#await getConfigs() then}
|
<div in:fade={{ duration: 300 }}>
|
||||||
<div in:fade={{ duration: 500 }}>
|
<form autocomplete="off" on:submit|preventDefault class="mx-4 flex flex-col gap-4 py-4">
|
||||||
<form autocomplete="off" on:submit|preventDefault class="mx-4 flex flex-col gap-4 py-4">
|
<p class="text-sm dark:text-immich-dark-fg">
|
||||||
<p class="text-sm dark:text-immich-dark-fg">
|
For more details about this feature, refer to the <a
|
||||||
For more details about this feature, refer to the <a
|
href="http://immich.app/docs/administration/oauth#mobile-redirect-uri"
|
||||||
href="http://immich.app/docs/administration/oauth#mobile-redirect-uri"
|
class="underline"
|
||||||
class="underline"
|
target="_blank"
|
||||||
target="_blank"
|
rel="noreferrer">docs</a
|
||||||
rel="noreferrer">docs</a
|
>.
|
||||||
>.
|
</p>
|
||||||
</p>
|
|
||||||
|
|
||||||
<SettingSwitch {disabled} title="ENABLE" bind:checked={oauthConfig.enabled} />
|
<SettingSwitch title="ENABLE" bind:checked={oauthConfig.enabled} />
|
||||||
<hr />
|
<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 SECRET"
|
||||||
|
bind:value={oauthConfig.clientSecret}
|
||||||
|
required={true}
|
||||||
|
disabled={disabled || !oauthConfig.enabled}
|
||||||
|
isEdited={!(oauthConfig.clientSecret == savedConfig.clientSecret)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingInputField
|
||||||
|
inputType={SettingInputFieldType.TEXT}
|
||||||
|
label="SCOPE"
|
||||||
|
bind:value={oauthConfig.scope}
|
||||||
|
required={true}
|
||||||
|
disabled={disabled || !oauthConfig.enabled}
|
||||||
|
isEdited={!(oauthConfig.scope == savedConfig.scope)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingInputField
|
||||||
|
inputType={SettingInputFieldType.TEXT}
|
||||||
|
label="STORAGE LABEL CLAIM"
|
||||||
|
desc="Automatically set the user's storage label to the value of this claim."
|
||||||
|
bind:value={oauthConfig.storageLabelClaim}
|
||||||
|
required={true}
|
||||||
|
disabled={disabled || !oauthConfig.storageLabelClaim}
|
||||||
|
isEdited={!(oauthConfig.storageLabelClaim == savedConfig.storageLabelClaim)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingInputField
|
||||||
|
inputType={SettingInputFieldType.TEXT}
|
||||||
|
label="BUTTON TEXT"
|
||||||
|
bind:value={oauthConfig.buttonText}
|
||||||
|
required={false}
|
||||||
|
disabled={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 LAUNCH"
|
||||||
|
subtitle="Start the OAuth login flow automatically upon navigating to the login page"
|
||||||
|
disabled={disabled || !oauthConfig.enabled}
|
||||||
|
bind:checked={oauthConfig.autoLaunch}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<SettingSwitch
|
||||||
|
title="MOBILE REDIRECT URI OVERRIDE"
|
||||||
|
subtitle="Enable when `app.immich:/` is an invalid redirect URI."
|
||||||
|
disabled={disabled || !oauthConfig.enabled}
|
||||||
|
on:click={() => handleToggleOverride()}
|
||||||
|
bind:checked={oauthConfig.mobileOverrideEnabled}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{#if oauthConfig.mobileOverrideEnabled}
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.TEXT}
|
inputType={SettingInputFieldType.TEXT}
|
||||||
label="ISSUER URL"
|
label="MOBILE REDIRECT URI"
|
||||||
bind:value={oauthConfig.issuerUrl}
|
bind:value={oauthConfig.mobileRedirectUri}
|
||||||
required={true}
|
required={true}
|
||||||
disabled={disabled || !oauthConfig.enabled}
|
disabled={disabled || !oauthConfig.enabled}
|
||||||
isEdited={!(oauthConfig.issuerUrl == savedConfig.issuerUrl)}
|
isEdited={!(oauthConfig.mobileRedirectUri == savedConfig.mobileRedirectUri)}
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<SettingInputField
|
<SettingButtonsRow
|
||||||
inputType={SettingInputFieldType.TEXT}
|
on:reset={reset}
|
||||||
label="CLIENT ID"
|
on:save={saveSetting}
|
||||||
bind:value={oauthConfig.clientId}
|
on:reset-to-default={resetToDefault}
|
||||||
required={true}
|
showResetToDefault={!isEqual(oauthConfig, oauthDefault)}
|
||||||
disabled={disabled || !oauthConfig.enabled}
|
{disabled}
|
||||||
isEdited={!(oauthConfig.clientId == savedConfig.clientId)}
|
/>
|
||||||
/>
|
</form>
|
||||||
|
</div>
|
||||||
<SettingInputField
|
|
||||||
inputType={SettingInputFieldType.TEXT}
|
|
||||||
label="CLIENT SECRET"
|
|
||||||
bind:value={oauthConfig.clientSecret}
|
|
||||||
required={true}
|
|
||||||
disabled={disabled || !oauthConfig.enabled}
|
|
||||||
isEdited={!(oauthConfig.clientSecret == savedConfig.clientSecret)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingInputField
|
|
||||||
inputType={SettingInputFieldType.TEXT}
|
|
||||||
label="SCOPE"
|
|
||||||
bind:value={oauthConfig.scope}
|
|
||||||
required={true}
|
|
||||||
disabled={disabled || !oauthConfig.enabled}
|
|
||||||
isEdited={!(oauthConfig.scope == savedConfig.scope)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingInputField
|
|
||||||
inputType={SettingInputFieldType.TEXT}
|
|
||||||
label="STORAGE LABEL CLAIM"
|
|
||||||
desc="Automatically set the user's storage label to the value of this claim."
|
|
||||||
bind:value={oauthConfig.storageLabelClaim}
|
|
||||||
required={true}
|
|
||||||
disabled={disabled || !oauthConfig.storageLabelClaim}
|
|
||||||
isEdited={!(oauthConfig.storageLabelClaim == savedConfig.storageLabelClaim)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingInputField
|
|
||||||
inputType={SettingInputFieldType.TEXT}
|
|
||||||
label="BUTTON TEXT"
|
|
||||||
bind:value={oauthConfig.buttonText}
|
|
||||||
required={false}
|
|
||||||
disabled={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={disabled || !oauthConfig.enabled}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingSwitch
|
|
||||||
title="AUTO LAUNCH"
|
|
||||||
subtitle="Start the OAuth login flow automatically upon navigating to the login page"
|
|
||||||
disabled={disabled || !oauthConfig.enabled}
|
|
||||||
bind:checked={oauthConfig.autoLaunch}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingSwitch
|
|
||||||
title="MOBILE REDIRECT URI OVERRIDE"
|
|
||||||
subtitle="Enable when `app.immich:/` is an invalid redirect URI."
|
|
||||||
disabled={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={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)}
|
|
||||||
{disabled}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
{/await}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -3,33 +3,33 @@
|
||||||
notificationController,
|
notificationController,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import type { SystemConfigDto, SystemConfigPasswordLoginDto } from '@api';
|
||||||
import { api, SystemConfigPasswordLoginDto } from '@api';
|
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import ConfirmDisableLogin from '../confirm-disable-login.svelte';
|
import ConfirmDisableLogin from '../confirm-disable-login.svelte';
|
||||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||||
import SettingSwitch from '../setting-switch.svelte';
|
import SettingSwitch from '../setting-switch.svelte';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
save: SystemConfigPasswordLoginDto;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
export let config: SystemConfigDto;
|
||||||
export let passwordLoginConfig: SystemConfigPasswordLoginDto; // this is the config that is being edited
|
export let passwordLoginConfig: SystemConfigPasswordLoginDto; // this is the config that is being edited
|
||||||
|
export let passwordLoginDefault: SystemConfigPasswordLoginDto;
|
||||||
|
export let savedConfig: SystemConfigPasswordLoginDto;
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
|
|
||||||
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),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
let isConfirmOpen = false;
|
let isConfirmOpen = false;
|
||||||
let handleConfirm: (value: boolean) => void;
|
let handleConfirm: (value: boolean) => void;
|
||||||
|
|
||||||
const openConfirmModal = () => {
|
const openConfirmModal = () => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
handleConfirm = (value: boolean) => {
|
handleConfirm = (value: boolean) => {
|
||||||
|
if (!value) {
|
||||||
|
passwordLoginConfig.enabled = !passwordLoginConfig.enabled;
|
||||||
|
}
|
||||||
isConfirmOpen = false;
|
isConfirmOpen = false;
|
||||||
resolve(value);
|
resolve(value);
|
||||||
};
|
};
|
||||||
|
@ -38,52 +38,30 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
async function saveSetting() {
|
async function saveSetting() {
|
||||||
try {
|
if (!config.oauth.enabled && savedConfig.enabled && !passwordLoginConfig.enabled) {
|
||||||
const { data: current } = await api.systemConfigApi.getConfig();
|
const confirmed = await openConfirmModal();
|
||||||
|
if (!confirmed) {
|
||||||
if (!current.oauth.enabled && current.passwordLogin.enabled && !passwordLoginConfig.enabled) {
|
return;
|
||||||
const confirmed = await openConfirmModal();
|
|
||||||
if (!confirmed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: updated } = await api.systemConfigApi.updateConfig({
|
|
||||||
systemConfigDto: {
|
|
||||||
...current,
|
|
||||||
passwordLogin: passwordLoginConfig,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
passwordLoginConfig = { ...updated.passwordLogin };
|
|
||||||
savedConfig = { ...updated.passwordLogin };
|
|
||||||
|
|
||||||
notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, 'Unable to save settings');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dispatch('save', passwordLoginConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reset() {
|
async function reset() {
|
||||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
passwordLoginConfig = { ...savedConfig };
|
||||||
|
|
||||||
passwordLoginConfig = { ...resetConfig.passwordLogin };
|
|
||||||
savedConfig = { ...resetConfig.passwordLogin };
|
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset settings to the recent saved settings',
|
message: 'Reset password authentication settings to the last saved settings',
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetToDefault() {
|
async function resetToDefault() {
|
||||||
const { data: configs } = await api.systemConfigApi.getDefaults();
|
passwordLoginConfig = { ...passwordLoginDefault };
|
||||||
|
|
||||||
passwordLoginConfig = { ...configs.passwordLogin };
|
|
||||||
defaultConfig = { ...configs.passwordLogin };
|
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset password settings to default',
|
message: 'Reset password authentication settings to default',
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -94,28 +72,27 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{#await getConfigs() then}
|
<div in:fade={{ duration: 300 }}>
|
||||||
<div in:fade={{ duration: 500 }}>
|
<form autocomplete="off" on:submit|preventDefault>
|
||||||
<form autocomplete="off" on:submit|preventDefault>
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<div class="ml-4">
|
||||||
<div class="ml-4">
|
<SettingSwitch
|
||||||
<SettingSwitch
|
title="ENABLED"
|
||||||
title="ENABLED"
|
{disabled}
|
||||||
{disabled}
|
subtitle="Login with email and password"
|
||||||
subtitle="Login with email and password"
|
isEdited={!(passwordLoginConfig.enabled == savedConfig.enabled)}
|
||||||
bind:checked={passwordLoginConfig.enabled}
|
bind:checked={passwordLoginConfig.enabled}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SettingButtonsRow
|
<SettingButtonsRow
|
||||||
on:reset={reset}
|
on:reset={reset}
|
||||||
on:save={saveSetting}
|
on:save={saveSetting}
|
||||||
on:reset-to-default={resetToDefault}
|
on:reset-to-default={resetToDefault}
|
||||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
showResetToDefault={!isEqual(passwordLoginConfig, passwordLoginDefault)}
|
||||||
{disabled}
|
{disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</form>
|
||||||
{/await}
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { api, SystemConfigStorageTemplateDto, SystemConfigTemplateStorageOptionDto, UserResponseDto } from '@api';
|
import type { SystemConfigStorageTemplateDto, SystemConfigTemplateStorageOptionDto, UserResponseDto } from '@api';
|
||||||
import * as luxon from 'luxon';
|
import * as luxon from 'luxon';
|
||||||
import handlebar from 'handlebars';
|
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 SupportedVariablesPanel from './supported-variables-panel.svelte';
|
||||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
|
@ -13,31 +10,20 @@
|
||||||
NotificationType,
|
NotificationType,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
save: SystemConfigStorageTemplateDto;
|
||||||
|
}>();
|
||||||
|
|
||||||
export let storageConfig: SystemConfigStorageTemplateDto;
|
export let storageConfig: SystemConfigStorageTemplateDto;
|
||||||
|
export let storageDefault: SystemConfigStorageTemplateDto;
|
||||||
export let user: UserResponseDto;
|
export let user: UserResponseDto;
|
||||||
export let disabled = false;
|
export let templateOptions: SystemConfigTemplateStorageOptionDto;
|
||||||
|
|
||||||
let savedConfig: SystemConfigStorageTemplateDto;
|
export let savedConfig: SystemConfigStorageTemplateDto;
|
||||||
let defaultConfig: SystemConfigStorageTemplateDto;
|
|
||||||
let templateOptions: SystemConfigTemplateStorageOptionDto;
|
|
||||||
let selectedPreset = '';
|
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 = savedConfig.template;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getSupportDateTimeFormat = async () => {
|
|
||||||
const { data } = await api.systemConfigApi.getStorageTemplateOptions();
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
$: parsedTemplate = () => {
|
$: parsedTemplate = () => {
|
||||||
try {
|
try {
|
||||||
return renderTemplate(storageConfig.template);
|
return renderTemplate(storageConfig.template);
|
||||||
|
@ -77,48 +63,15 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
async function reset() {
|
async function reset() {
|
||||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
storageConfig = { ...savedConfig };
|
||||||
|
|
||||||
storageConfig.template = resetConfig.storageTemplate.template;
|
|
||||||
savedConfig.template = resetConfig.storageTemplate.template;
|
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset storage template settings to the recent saved settings',
|
message: 'Reset storage template settings to the last saved settings',
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSetting() {
|
|
||||||
try {
|
|
||||||
const { data: currentConfig } = await api.systemConfigApi.getConfig();
|
|
||||||
|
|
||||||
const result = await api.systemConfigApi.updateConfig({
|
|
||||||
systemConfigDto: {
|
|
||||||
...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() {
|
async function resetToDefault() {
|
||||||
const { data: defaultConfig } = await api.systemConfigApi.getDefaults();
|
storageConfig = { ...storageDefault };
|
||||||
|
|
||||||
storageConfig.template = defaultConfig.storageTemplate.template;
|
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset storage template to default',
|
message: 'Reset storage template to default',
|
||||||
|
@ -132,97 +85,82 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="dark:text-immich-dark-fg">
|
<section class="dark:text-immich-dark-fg">
|
||||||
{#await getConfigs() then}
|
<div id="directory-path-builder" class="m-4">
|
||||||
<div id="directory-path-builder" class="m-4">
|
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">Variables</h3>
|
||||||
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">Variables</h3>
|
|
||||||
|
|
||||||
<section class="support-date">
|
<section class="support-date" />
|
||||||
{#await getSupportDateTimeFormat()}
|
|
||||||
<LoadingSpinner />
|
<section class="support-date">
|
||||||
{:then options}
|
<SupportedVariablesPanel />
|
||||||
<div transition:fade={{ duration: 200 }}>
|
</section>
|
||||||
<SupportedDatetimePanel {options} />
|
|
||||||
|
<div class="mt-4 flex flex-col">
|
||||||
|
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">Template</h3>
|
||||||
|
|
||||||
|
<div class="my-2 text-xs">
|
||||||
|
<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">
|
||||||
|
<code>{user.storageLabel || user.id}</code> is the user's Storage Label
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p class="mt-2 rounded-lg bg-gray-200 p-4 py-2 text-xs dark:bg-gray-700 dark:text-immich-dark-fg">
|
||||||
|
<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="my-2 flex flex-col">
|
||||||
|
<label class="text-xs" for="preset-select">PRESET</label>
|
||||||
|
<select
|
||||||
|
class="mt-2 rounded-lg bg-slate-200 p-2 text-sm hover:cursor-pointer dark:bg-gray-600"
|
||||||
|
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>
|
||||||
{/await}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="support-date">
|
|
||||||
<SupportedVariablesPanel />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="mt-4 flex flex-col">
|
|
||||||
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">Template</h3>
|
|
||||||
|
|
||||||
<div class="my-2 text-xs">
|
|
||||||
<h4>PREVIEW</h4>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="text-xs">
|
<div id="migration-info" class="mt-4 text-sm">
|
||||||
Approximately path length limit : <span
|
<p>
|
||||||
class="font-semibold text-immich-primary dark:text-immich-dark-primary"
|
Template changes will only apply to new assets. To retroactively apply the template to previously uploaded
|
||||||
>{parsedTemplate().length + user.id.length + 'UPLOAD_LOCATION'.length}</span
|
assets, run the <a href="/admin/jobs-status" class="text-immich-primary dark:text-immich-dark-primary"
|
||||||
>/260
|
>Storage Migration Job</a
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="text-xs">
|
|
||||||
<code>{user.storageLabel || user.id}</code> is the user's Storage Label
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="mt-2 rounded-lg bg-gray-200 p-4 py-2 text-xs dark:bg-gray-700 dark:text-immich-dark-fg">
|
|
||||||
<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="my-2 flex flex-col">
|
|
||||||
<label class="text-xs" for="preset-select">PRESET</label>
|
|
||||||
<select
|
|
||||||
class="mt-2 rounded-lg bg-slate-200 p-2 text-sm hover:cursor-pointer dark:bg-gray-600"
|
|
||||||
{disabled}
|
|
||||||
name="presets"
|
|
||||||
id="preset-select"
|
|
||||||
bind:value={selectedPreset}
|
|
||||||
on:change={handlePresetSelection}
|
|
||||||
>
|
>
|
||||||
{#each templateOptions.presetOptions as preset}
|
</p>
|
||||||
<option value={preset}>{renderTemplate(preset)}</option>
|
</div>
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-2 align-bottom">
|
|
||||||
<SettingInputField
|
|
||||||
label="TEMPLATE"
|
|
||||||
{disabled}
|
|
||||||
required
|
|
||||||
inputType={SettingInputFieldType.TEXT}
|
|
||||||
bind:value={storageConfig.template}
|
|
||||||
isEdited={!(storageConfig.template === savedConfig.template)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div class="flex-0">
|
<SettingButtonsRow
|
||||||
<SettingInputField label="EXTENSION" inputType={SettingInputFieldType.TEXT} value={'.jpg'} disabled />
|
on:reset={reset}
|
||||||
</div>
|
on:save={() => dispatch('save', savedConfig)}
|
||||||
</div>
|
on:reset-to-default={resetToDefault}
|
||||||
|
showResetToDefault={!isEqual(savedConfig, storageConfig)}
|
||||||
<div id="migration-info" class="mt-4 text-sm">
|
/>
|
||||||
<p>
|
</form>
|
||||||
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)}
|
|
||||||
{disabled}
|
|
||||||
/>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SettingSelect from '$lib/components/admin-page/settings/setting-select.svelte';
|
import SettingSelect from '$lib/components/admin-page/settings/setting-select.svelte';
|
||||||
import { api, SystemConfigThumbnailDto } from '@api';
|
import type { SystemConfigThumbnailDto } from '@api';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import SettingButtonsRow from '$lib/components/admin-page/settings/setting-buttons-row.svelte';
|
import SettingButtonsRow from '$lib/components/admin-page/settings/setting-buttons-row.svelte';
|
||||||
|
@ -8,118 +8,80 @@
|
||||||
notificationController,
|
notificationController,
|
||||||
NotificationType,
|
NotificationType,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher<{
|
||||||
|
save: SystemConfigThumbnailDto;
|
||||||
|
}>();
|
||||||
|
|
||||||
export let thumbnailConfig: SystemConfigThumbnailDto; // this is the config that is being edited
|
export let thumbnailConfig: SystemConfigThumbnailDto; // this is the config that is being edited
|
||||||
|
export let thumbnailDefault: SystemConfigThumbnailDto;
|
||||||
|
export let savedConfig: SystemConfigThumbnailDto;
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
|
|
||||||
let savedConfig: SystemConfigThumbnailDto;
|
|
||||||
let defaultConfig: SystemConfigThumbnailDto;
|
|
||||||
|
|
||||||
async function getConfigs() {
|
|
||||||
[savedConfig, defaultConfig] = await Promise.all([
|
|
||||||
api.systemConfigApi.getConfig().then((res) => res.data.thumbnail),
|
|
||||||
api.systemConfigApi.getDefaults().then((res) => res.data.thumbnail),
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reset() {
|
async function reset() {
|
||||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
thumbnailConfig = { ...savedConfig };
|
||||||
|
|
||||||
thumbnailConfig = { ...resetConfig.thumbnail };
|
|
||||||
savedConfig = { ...resetConfig.thumbnail };
|
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset thumbnail settings to the recent saved settings',
|
message: 'Reset thumbnail settings to the last saved settings',
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetToDefault() {
|
async function resetToDefault() {
|
||||||
const { data: configs } = await api.systemConfigApi.getDefaults();
|
thumbnailConfig = { ...thumbnailDefault };
|
||||||
|
|
||||||
thumbnailConfig = { ...configs.thumbnail };
|
|
||||||
defaultConfig = { ...configs.thumbnail };
|
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset thumbnail settings to default',
|
message: 'Reset thumbnail settings to default',
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveSetting() {
|
|
||||||
try {
|
|
||||||
const { data: configs } = await api.systemConfigApi.getConfig();
|
|
||||||
|
|
||||||
const result = await api.systemConfigApi.updateConfig({
|
|
||||||
systemConfigDto: {
|
|
||||||
...configs,
|
|
||||||
thumbnail: thumbnailConfig,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
thumbnailConfig = { ...result.data.thumbnail };
|
|
||||||
savedConfig = { ...result.data.thumbnail };
|
|
||||||
|
|
||||||
notificationController.show({
|
|
||||||
message: 'Thumbnail settings saved',
|
|
||||||
type: NotificationType.Info,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error [thumbnail-settings] [saveSetting]', e);
|
|
||||||
notificationController.show({
|
|
||||||
message: 'Unable to save settings',
|
|
||||||
type: NotificationType.Error,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
{#await getConfigs() then}
|
<div in:fade={{ duration: 300 }}>
|
||||||
<div in:fade={{ duration: 500 }}>
|
<form autocomplete="off" on:submit|preventDefault>
|
||||||
<form autocomplete="off" on:submit|preventDefault>
|
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<SettingSelect
|
||||||
<SettingSelect
|
label="SMALL THUMBNAIL RESOLUTION"
|
||||||
label="SMALL THUMBNAIL RESOLUTION"
|
desc="Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
||||||
desc="Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
number
|
||||||
number
|
bind:value={thumbnailConfig.webpSize}
|
||||||
bind:value={thumbnailConfig.webpSize}
|
options={[
|
||||||
options={[
|
{ value: 1080, text: '1080p' },
|
||||||
{ value: 1080, text: '1080p' },
|
{ value: 720, text: '720p' },
|
||||||
{ value: 720, text: '720p' },
|
{ value: 480, text: '480p' },
|
||||||
{ value: 480, text: '480p' },
|
{ value: 250, text: '250p' },
|
||||||
{ value: 250, text: '250p' },
|
]}
|
||||||
]}
|
name="resolution"
|
||||||
name="resolution"
|
isEdited={!(thumbnailConfig.webpSize === savedConfig.webpSize)}
|
||||||
isEdited={!(thumbnailConfig.webpSize === savedConfig.webpSize)}
|
{disabled}
|
||||||
{disabled}
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<SettingSelect
|
<SettingSelect
|
||||||
label="LARGE THUMBNAIL RESOLUTION"
|
label="LARGE THUMBNAIL RESOLUTION"
|
||||||
desc="Used when viewing a single photo and for machine learning. Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
desc="Used when viewing a single photo and for machine learning. Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
||||||
number
|
number
|
||||||
bind:value={thumbnailConfig.jpegSize}
|
bind:value={thumbnailConfig.jpegSize}
|
||||||
options={[
|
options={[
|
||||||
{ value: 2160, text: '4K' },
|
{ value: 2160, text: '4K' },
|
||||||
{ value: 1440, text: '1440p' },
|
{ value: 1440, text: '1440p' },
|
||||||
]}
|
]}
|
||||||
name="resolution"
|
name="resolution"
|
||||||
isEdited={!(thumbnailConfig.jpegSize === savedConfig.jpegSize)}
|
isEdited={!(thumbnailConfig.jpegSize === savedConfig.jpegSize)}
|
||||||
{disabled}
|
{disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<SettingButtonsRow
|
<SettingButtonsRow
|
||||||
on:reset={reset}
|
on:reset={reset}
|
||||||
on:save={saveSetting}
|
on:save={() => dispatch('save', thumbnailConfig)}
|
||||||
on:reset-to-default={resetToDefault}
|
on:reset-to-default={resetToDefault}
|
||||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
showResetToDefault={!isEqual(thumbnailConfig, thumbnailDefault)}
|
||||||
{disabled}
|
{disabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { AppRoute } from '$lib/constants';
|
||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ parent }) => {
|
export const load: PageServerLoad = async ({ parent, locals }) => {
|
||||||
const { user } = await parent();
|
const { user } = await parent();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
@ -11,8 +11,14 @@ export const load: PageServerLoad = async ({ parent }) => {
|
||||||
throw redirect(302, AppRoute.PHOTOS);
|
throw redirect(302, AppRoute.PHOTOS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { data: config } = await locals.api.systemConfigApi.getConfig();
|
||||||
|
const { data: defaultConfig } = await locals.api.systemConfigApi.getDefaults();
|
||||||
|
const { data: templateOptions } = await locals.api.systemConfigApi.getStorageTemplateOptions();
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
|
config,
|
||||||
|
defaultConfig,
|
||||||
|
templateOptions,
|
||||||
meta: {
|
meta: {
|
||||||
title: 'System Settings',
|
title: 'System Settings',
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,21 +9,46 @@
|
||||||
import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte';
|
import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte';
|
||||||
import ThumbnailSettings from '$lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte';
|
import ThumbnailSettings from '$lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte';
|
||||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
|
||||||
import { downloadManager } from '$lib/stores/download';
|
import { downloadManager } from '$lib/stores/download';
|
||||||
import { featureFlags } from '$lib/stores/feature-flags.store';
|
import { featureFlags } from '$lib/stores/feature-flags.store';
|
||||||
import { downloadBlob } from '$lib/utils/asset-utils';
|
import { downloadBlob } from '$lib/utils/asset-utils';
|
||||||
import { SystemConfigDto, api, copyToClipboard } from '@api';
|
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, api, copyToClipboard } from '@api';
|
||||||
import Alert from 'svelte-material-icons/Alert.svelte';
|
import Alert from 'svelte-material-icons/Alert.svelte';
|
||||||
import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
|
import ContentCopy from 'svelte-material-icons/ContentCopy.svelte';
|
||||||
import Download from 'svelte-material-icons/Download.svelte';
|
import Download from 'svelte-material-icons/Download.svelte';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
import {
|
||||||
|
notificationController,
|
||||||
|
NotificationType,
|
||||||
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
|
import { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
let config: SystemConfigDto = data.config;
|
||||||
|
|
||||||
const getConfig = async () => {
|
let currentConfig: SystemConfigDto = cloneDeep(config);
|
||||||
const { data } = await api.systemConfigApi.getConfig();
|
let defaultConfig: SystemConfigDto = data.defaultConfig;
|
||||||
return data;
|
let templateOptions: SystemConfigTemplateStorageOptionDto = data.templateOptions;
|
||||||
|
|
||||||
|
const handleSave = async (config: SystemConfigDto, settingsMessage: string) => {
|
||||||
|
try {
|
||||||
|
const { data } = await api.systemConfigApi.updateConfig({
|
||||||
|
systemConfigDto: config,
|
||||||
|
});
|
||||||
|
|
||||||
|
config = cloneDeep(data);
|
||||||
|
currentConfig = cloneDeep(data);
|
||||||
|
notificationController.show({
|
||||||
|
message: `${settingsMessage} settings saved`,
|
||||||
|
type: NotificationType.Info,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Error [${settingsMessage}-settings] [saveSetting]`, e);
|
||||||
|
notificationController.show({
|
||||||
|
message: 'Unable to save settings',
|
||||||
|
type: NotificationType.Error,
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadConfig = (configs: SystemConfigDto) => {
|
const downloadConfig = (configs: SystemConfigDto) => {
|
||||||
|
@ -42,62 +67,147 @@
|
||||||
<h2 class="text-md text-immich-primary dark:text-immich-dark-primary">Config is currently set by a config file</h2>
|
<h2 class="text-md text-immich-primary dark:text-immich-dark-primary">Config is currently set by a config file</h2>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<Button size="sm" on:click={() => copyToClipboard(JSON.stringify(config, null, 2))}>
|
||||||
|
<ContentCopy size="18" />
|
||||||
|
<span class="pl-2">Copy to Clipboard</span>
|
||||||
|
</Button>
|
||||||
|
<Button size="sm" on:click={() => downloadConfig(config)}>
|
||||||
|
<Download size="18" />
|
||||||
|
<span class="pl-2">Export as JSON</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<SettingAccordion title="Thumbnail Settings" subtitle="Manage the resolution of thumbnail sizes">
|
||||||
|
<ThumbnailSettings
|
||||||
|
bind:savedConfig={currentConfig.thumbnail}
|
||||||
|
bind:thumbnailConfig={config.thumbnail}
|
||||||
|
thumbnailDefault={defaultConfig.thumbnail}
|
||||||
|
disabled={$featureFlags.configFile}
|
||||||
|
on:save={({ detail: thumbnail }) => {
|
||||||
|
handleSave(
|
||||||
|
{
|
||||||
|
...currentConfig,
|
||||||
|
thumbnail,
|
||||||
|
},
|
||||||
|
'Thumbnail',
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingAccordion>
|
||||||
|
|
||||||
<section class="">
|
<SettingAccordion title="FFmpeg Settings" subtitle="Manage the resolution and encoding information of the video files">
|
||||||
{#await getConfig()}
|
<FFmpegSettings
|
||||||
<LoadingSpinner />
|
bind:savedConfig={currentConfig.ffmpeg}
|
||||||
{:then configs}
|
bind:ffmpegConfig={config.ffmpeg}
|
||||||
<div class="flex justify-end gap-2">
|
ffmpegDefault={defaultConfig.ffmpeg}
|
||||||
<Button size="sm" on:click={() => copyToClipboard(JSON.stringify(configs, null, 2))}>
|
disabled={$featureFlags.configFile}
|
||||||
<ContentCopy size="18" />
|
on:save={({ detail: ffmpeg }) => {
|
||||||
<span class="pl-2">Copy to Clipboard</span>
|
handleSave(
|
||||||
</Button>
|
{
|
||||||
<Button size="sm" on:click={() => downloadConfig(configs)}>
|
...currentConfig,
|
||||||
<Download size="18" />
|
ffmpeg,
|
||||||
<span class="pl-2">Export as JSON</span>
|
},
|
||||||
</Button>
|
'FFmpeg',
|
||||||
</div>
|
);
|
||||||
<SettingAccordion title="Thumbnail Settings" subtitle="Manage the resolution of thumbnail sizes">
|
}}
|
||||||
<ThumbnailSettings disabled={$featureFlags.configFile} thumbnailConfig={configs.thumbnail} />
|
/>
|
||||||
</SettingAccordion>
|
</SettingAccordion>
|
||||||
|
|
||||||
<SettingAccordion
|
<SettingAccordion
|
||||||
title="FFmpeg Settings"
|
title="Job Settings"
|
||||||
subtitle="Manage the resolution and encoding information of the video files"
|
subtitle="Manage job concurrency"
|
||||||
>
|
isOpen={$page.url.searchParams.get('open') === 'job-settings'}
|
||||||
<FFmpegSettings disabled={$featureFlags.configFile} ffmpegConfig={configs.ffmpeg} />
|
>
|
||||||
</SettingAccordion>
|
<JobSettings
|
||||||
|
bind:savedConfig={currentConfig.job}
|
||||||
|
bind:jobConfig={config.job}
|
||||||
|
jobDefault={defaultConfig.job}
|
||||||
|
disabled={$featureFlags.configFile}
|
||||||
|
on:save={({ detail: job }) => {
|
||||||
|
handleSave(
|
||||||
|
{
|
||||||
|
...currentConfig,
|
||||||
|
job,
|
||||||
|
},
|
||||||
|
'Job Settings',
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingAccordion>
|
||||||
|
|
||||||
<SettingAccordion title="Machine Learning Settings" subtitle="Manage model settings">
|
<SettingAccordion title="Password Authentication" subtitle="Manage login with password settings">
|
||||||
<MachineLearningSettings disabled={$featureFlags.configFile} machineLearningConfig={configs.machineLearning} />
|
<PasswordLoginSettings
|
||||||
</SettingAccordion>
|
bind:savedConfig={currentConfig.passwordLogin}
|
||||||
|
bind:passwordLoginConfig={config.passwordLogin}
|
||||||
|
bind:config
|
||||||
|
disabled={$featureFlags.configFile}
|
||||||
|
passwordLoginDefault={defaultConfig.passwordLogin}
|
||||||
|
on:save={({ detail: passwordLogin }) => {
|
||||||
|
handleSave(
|
||||||
|
{
|
||||||
|
...currentConfig,
|
||||||
|
passwordLogin,
|
||||||
|
},
|
||||||
|
'Password Authentication',
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingAccordion>
|
||||||
|
<SettingAccordion title="Machine Learning" subtitle="Manage machine learning settings">
|
||||||
|
<MachineLearningSettings
|
||||||
|
bind:savedConfig={currentConfig.machineLearning}
|
||||||
|
bind:machineLearningConfig={config.machineLearning}
|
||||||
|
machineLearningDefault={defaultConfig.machineLearning}
|
||||||
|
disabled={$featureFlags.configFile}
|
||||||
|
on:save={({ detail: machineLearning }) => {
|
||||||
|
handleSave(
|
||||||
|
{
|
||||||
|
...currentConfig,
|
||||||
|
machineLearning,
|
||||||
|
},
|
||||||
|
'Machine Learning',
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingAccordion>
|
||||||
|
<SettingAccordion title="OAuth Authentication" subtitle="Manage the login with OAuth settings">
|
||||||
|
<OAuthSettings
|
||||||
|
bind:savedConfig={currentConfig.oauth}
|
||||||
|
bind:oauthConfig={config.oauth}
|
||||||
|
bind:config
|
||||||
|
disabled={$featureFlags.configFile}
|
||||||
|
oauthDefault={defaultConfig.oauth}
|
||||||
|
on:save={({ detail: oauth }) => {
|
||||||
|
handleSave(
|
||||||
|
{
|
||||||
|
...currentConfig,
|
||||||
|
oauth,
|
||||||
|
},
|
||||||
|
'OAuth Authentication',
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingAccordion>
|
||||||
|
|
||||||
<SettingAccordion
|
<SettingAccordion
|
||||||
title="Job Settings"
|
title="Storage Template"
|
||||||
subtitle="Manage job concurrency"
|
subtitle="Manage the folder structure and file name of the upload asset"
|
||||||
isOpen={$page.url.searchParams.get('open') === 'job-settings'}
|
isOpen={$page.url.searchParams.get('open') === 'storage-template'}
|
||||||
>
|
>
|
||||||
<JobSettings disabled={$featureFlags.configFile} jobConfig={configs.job} />
|
<StorageTemplateSettings
|
||||||
</SettingAccordion>
|
savedConfig={currentConfig.storageTemplate}
|
||||||
|
storageConfig={config.storageTemplate}
|
||||||
<SettingAccordion title="Password Authentication" subtitle="Manage login with password settings">
|
user={data.user}
|
||||||
<PasswordLoginSettings disabled={$featureFlags.configFile} passwordLoginConfig={configs.passwordLogin} />
|
storageDefault={defaultConfig.storageTemplate}
|
||||||
</SettingAccordion>
|
{templateOptions}
|
||||||
|
on:save={({ detail: storageTemplate }) => {
|
||||||
<SettingAccordion title="OAuth Authentication" subtitle="Manage the login with OAuth settings">
|
handleSave(
|
||||||
<OAuthSettings disabled={$featureFlags.configFile} oauthConfig={configs.oauth} />
|
{
|
||||||
</SettingAccordion>
|
...currentConfig,
|
||||||
|
storageTemplate,
|
||||||
<SettingAccordion
|
},
|
||||||
title="Storage Template"
|
'Storage Template',
|
||||||
subtitle="Manage the folder structure and file name of the upload asset"
|
);
|
||||||
isOpen={$page.url.searchParams.get('open') === 'storage-template'}
|
}}
|
||||||
>
|
/>
|
||||||
<StorageTemplateSettings
|
</SettingAccordion>
|
||||||
disabled={$featureFlags.configFile}
|
|
||||||
storageConfig={configs.storageTemplate}
|
|
||||||
user={data.user}
|
|
||||||
/>
|
|
||||||
</SettingAccordion>
|
|
||||||
{/await}
|
|
||||||
</section>
|
|
||||||
|
|
Loading…
Reference in a new issue