mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
refactor(web): admin settings (#6177)
* refactor admin settings * use slots to render buttons in simplified template settings * remove more boilerplate by looping over components * fix: onboarding * fix: reset/reset to default * remove lodash since it is unecessary * chore: standardize padding and margins --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
parent
2439c5ab57
commit
a4f49d197e
@ -3,7 +3,7 @@
|
||||
</script>
|
||||
|
||||
Apply the current
|
||||
<a href={`${AppRoute.ADMIN_SETTINGS}?open=storage-template`} class="text-immich-primary dark:text-immich-dark-primary"
|
||||
<a href={`${AppRoute.ADMIN_SETTINGS}?open=storageTemplate`} class="text-immich-primary dark:text-immich-dark-primary"
|
||||
>Storage template</a
|
||||
>
|
||||
to previously uploaded assets
|
||||
|
@ -0,0 +1,75 @@
|
||||
<svelte:options accessors />
|
||||
|
||||
<script lang="ts">
|
||||
import { SystemConfigDto, api } from '@api';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import type { SettingsEventType } from './admin-settings';
|
||||
import { createEventDispatcher, onMount } from 'svelte';
|
||||
|
||||
export let config: SystemConfigDto;
|
||||
|
||||
let savedConfig: SystemConfigDto;
|
||||
let defaultConfig: SystemConfigDto;
|
||||
|
||||
const dispatch = createEventDispatcher<{ save: void }>();
|
||||
|
||||
const handleReset = async (detail: SettingsEventType['reset']) => {
|
||||
if (detail.default) {
|
||||
await resetToDefault(detail.configKeys);
|
||||
} else {
|
||||
await reset(detail.configKeys);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async (config: Partial<SystemConfigDto>) => {
|
||||
try {
|
||||
const result = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...savedConfig,
|
||||
...config,
|
||||
},
|
||||
});
|
||||
|
||||
savedConfig = { ...result.data };
|
||||
notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
|
||||
|
||||
dispatch('save');
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save settings');
|
||||
}
|
||||
};
|
||||
|
||||
const reset = async (configKeys: Array<keyof SystemConfigDto>) => {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
config = configKeys.reduce((acc, key) => ({ ...acc, [key]: resetConfig[key] }), config);
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset settings to the recent saved settings',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
};
|
||||
|
||||
const resetToDefault = async (configKeys: Array<keyof SystemConfigDto>) => {
|
||||
config = configKeys.reduce((acc, key) => ({ ...acc, [key]: defaultConfig[key] }), config);
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset settings to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
};
|
||||
|
||||
onMount(async () => {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data),
|
||||
api.systemConfigApi.getConfigDefaults().then((res) => res.data),
|
||||
]);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if savedConfig && defaultConfig}
|
||||
<slot {handleReset} {handleSave} {savedConfig} {defaultConfig} />
|
||||
{/if}
|
@ -0,0 +1,7 @@
|
||||
import type { ResetOptions } from '$lib/utils/dipatch';
|
||||
import type { SystemConfigDto } from '@api';
|
||||
|
||||
export type SettingsEventType = {
|
||||
reset: ResetOptions & { configKeys: Array<keyof SystemConfigDto> };
|
||||
save: Partial<SystemConfigDto>;
|
||||
};
|
@ -1,13 +1,8 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import {
|
||||
api,
|
||||
AudioCodec,
|
||||
CQMode,
|
||||
SystemConfigFFmpegDto,
|
||||
SystemConfigDto,
|
||||
ToneMapping,
|
||||
TranscodeHWAccel,
|
||||
TranscodePolicy,
|
||||
@ -22,354 +17,288 @@
|
||||
import SettingAccordion from '../setting-accordion.svelte';
|
||||
import { mdiHelpCircleOutline } from '@mdi/js';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import type { ResetOptions } from '$lib/utils/dipatch';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { SettingsEventType } from '../admin-settings';
|
||||
|
||||
export let ffmpegConfig: SystemConfigFFmpegDto; // this is the config that is being edited
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
export let config: SystemConfigDto; // this is the config that is being edited
|
||||
export let disabled = false;
|
||||
|
||||
let savedConfig: SystemConfigFFmpegDto;
|
||||
let defaultConfig: SystemConfigFFmpegDto;
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.ffmpeg),
|
||||
api.systemConfigApi.getConfigDefaults().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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const handleReset = (detail: ResetOptions) => {
|
||||
if (detail.default) {
|
||||
resetToDefault();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
ffmpegConfig = { ...resetConfig.ffmpeg };
|
||||
savedConfig = { ...resetConfig.ffmpeg };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset FFmpeg settings to the recent saved settings',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
|
||||
|
||||
ffmpegConfig = { ...configs.ffmpeg };
|
||||
defaultConfig = { ...configs.ffmpeg };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset FFmpeg settings to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
const dispatch = createEventDispatcher<SettingsEventType>();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
<Icon path={mdiHelpCircleOutline} class="inline" size="15" />
|
||||
To learn more about the terminology used here, refer to FFmpeg documentation for
|
||||
<a href="https://trac.ffmpeg.org/wiki/Encode/H.264" class="underline" target="_blank" rel="noreferrer"
|
||||
>H.264 codec</a
|
||||
>,
|
||||
<a href="https://trac.ffmpeg.org/wiki/Encode/H.265" class="underline" target="_blank" rel="noreferrer"
|
||||
>HEVC codec</a
|
||||
>
|
||||
and
|
||||
<a href="https://trac.ffmpeg.org/wiki/Encode/VP9" class="underline" target="_blank" rel="noreferrer"
|
||||
>VP9 codec</a
|
||||
>.
|
||||
</p>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
{disabled}
|
||||
label="CONSTANT RATE FACTOR (-crf)"
|
||||
desc="Video quality level. Typical values are 23 for H.264, 28 for HEVC, and 31 for VP9. Lower is better, but takes longer to encode and produces larger files."
|
||||
bind:value={ffmpegConfig.crf}
|
||||
required={true}
|
||||
isEdited={ffmpegConfig.crf !== savedConfig.crf}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="PRESET (-preset)"
|
||||
{disabled}
|
||||
desc="Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above `faster`."
|
||||
bind:value={ffmpegConfig.preset}
|
||||
name="preset"
|
||||
options={[
|
||||
{ value: 'ultrafast', text: 'ultrafast' },
|
||||
{ value: 'superfast', text: 'superfast' },
|
||||
{ value: 'veryfast', text: 'veryfast' },
|
||||
{ value: 'faster', text: 'faster' },
|
||||
{ value: 'fast', text: 'fast' },
|
||||
{ value: 'medium', text: 'medium' },
|
||||
{ value: 'slow', text: 'slow' },
|
||||
{ value: 'slower', text: 'slower' },
|
||||
{ value: 'veryslow', text: 'veryslow' },
|
||||
]}
|
||||
isEdited={ffmpegConfig.preset !== savedConfig.preset}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="AUDIO CODEC"
|
||||
{disabled}
|
||||
desc="Opus is the highest quality option, but has lower compatibility with old devices or software."
|
||||
bind:value={ffmpegConfig.targetAudioCodec}
|
||||
options={[
|
||||
{ value: AudioCodec.Aac, text: 'aac' },
|
||||
{ value: AudioCodec.Mp3, text: 'mp3' },
|
||||
{ value: AudioCodec.Libopus, text: 'opus' },
|
||||
]}
|
||||
name="acodec"
|
||||
isEdited={ffmpegConfig.targetAudioCodec !== savedConfig.targetAudioCodec}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="VIDEO CODEC"
|
||||
{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."
|
||||
bind:value={ffmpegConfig.targetVideoCodec}
|
||||
options={[
|
||||
{ value: VideoCodec.H264, text: 'h264' },
|
||||
{ value: VideoCodec.Hevc, text: 'hevc' },
|
||||
{ value: VideoCodec.Vp9, text: 'vp9' },
|
||||
]}
|
||||
name="vcodec"
|
||||
isEdited={ffmpegConfig.targetVideoCodec !== savedConfig.targetVideoCodec}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="TARGET RESOLUTION"
|
||||
{disabled}
|
||||
desc="Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
||||
bind:value={ffmpegConfig.targetResolution}
|
||||
options={[
|
||||
{ value: '2160', text: '4k' },
|
||||
{ value: '1440', text: '1440p' },
|
||||
{ value: '1080', text: '1080p' },
|
||||
{ value: '720', text: '720p' },
|
||||
{ value: '480', text: '480p' },
|
||||
{ value: 'original', text: 'original' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={ffmpegConfig.targetResolution !== savedConfig.targetResolution}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
{disabled}
|
||||
label="MAX BITRATE"
|
||||
desc="Setting a max bitrate can make file sizes more predictable at a minor cost to quality. At 720p, typical values are 2600k for VP9 or HEVC, or 4500k for H.264. Disabled if set to 0."
|
||||
bind:value={ffmpegConfig.maxBitrate}
|
||||
isEdited={ffmpegConfig.maxBitrate !== savedConfig.maxBitrate}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
{disabled}
|
||||
label="THREADS"
|
||||
desc="Higher values lead to faster encoding, but leave less room for the server to process other tasks while active. This value should not be more than the number of CPU cores. Maximizes utilization if set to 0."
|
||||
bind:value={ffmpegConfig.threads}
|
||||
isEdited={ffmpegConfig.threads !== savedConfig.threads}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="TRANSCODE POLICY"
|
||||
{disabled}
|
||||
desc="Policy for when a video should be transcoded."
|
||||
bind:value={ffmpegConfig.transcode}
|
||||
name="transcode"
|
||||
options={[
|
||||
{ value: TranscodePolicy.All, text: 'All videos' },
|
||||
{
|
||||
value: TranscodePolicy.Optimal,
|
||||
text: 'Videos higher than target resolution or not in the desired format',
|
||||
},
|
||||
{
|
||||
value: TranscodePolicy.Required,
|
||||
text: 'Only videos not in the desired format',
|
||||
},
|
||||
{
|
||||
value: TranscodePolicy.Disabled,
|
||||
text: "Don't transcode any videos, may break playback on some clients",
|
||||
},
|
||||
]}
|
||||
isEdited={ffmpegConfig.transcode !== savedConfig.transcode}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="TONE-MAPPING"
|
||||
{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."
|
||||
bind:value={ffmpegConfig.tonemap}
|
||||
name="tonemap"
|
||||
options={[
|
||||
{
|
||||
value: ToneMapping.Hable,
|
||||
text: 'Hable',
|
||||
},
|
||||
{
|
||||
value: ToneMapping.Mobius,
|
||||
text: 'Mobius',
|
||||
},
|
||||
{
|
||||
value: ToneMapping.Reinhard,
|
||||
text: 'Reinhard',
|
||||
},
|
||||
{
|
||||
value: ToneMapping.Disabled,
|
||||
text: 'Disabled',
|
||||
},
|
||||
]}
|
||||
isEdited={ffmpegConfig.tonemap !== savedConfig.tonemap}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="TWO-PASS ENCODING"
|
||||
{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}
|
||||
isEdited={ffmpegConfig.twoPass !== savedConfig.twoPass}
|
||||
/>
|
||||
|
||||
<SettingAccordion
|
||||
title="Hardware Acceleration"
|
||||
subtitle="Experimental; much faster, but will have lower quality at the same bitrate"
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
<Icon path={mdiHelpCircleOutline} class="inline" size="15" />
|
||||
To learn more about the terminology used here, refer to FFmpeg documentation for
|
||||
<a href="https://trac.ffmpeg.org/wiki/Encode/H.264" class="underline" target="_blank" rel="noreferrer"
|
||||
>H.264 codec</a
|
||||
>,
|
||||
<a href="https://trac.ffmpeg.org/wiki/Encode/H.265" class="underline" target="_blank" rel="noreferrer"
|
||||
>HEVC codec</a
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSelect
|
||||
label="ACCELERATION API"
|
||||
{disabled}
|
||||
desc="The API that will interact with your device to accelerate transcoding. This setting is 'best effort': it will fallback to software transcoding on failure. VP9 may or may not work depending on your hardware."
|
||||
bind:value={ffmpegConfig.accel}
|
||||
name="accel"
|
||||
options={[
|
||||
{ value: TranscodeHWAccel.Nvenc, text: 'NVENC (requires NVIDIA GPU)' },
|
||||
{
|
||||
value: TranscodeHWAccel.Qsv,
|
||||
text: 'Quick Sync (requires 7th gen Intel CPU or later)',
|
||||
},
|
||||
{
|
||||
value: TranscodeHWAccel.Vaapi,
|
||||
text: 'VAAPI',
|
||||
},
|
||||
{
|
||||
value: TranscodeHWAccel.Rkmpp,
|
||||
text: 'RKMPP (only on Rockchip SOCs)',
|
||||
},
|
||||
{
|
||||
value: TranscodeHWAccel.Disabled,
|
||||
text: 'Disabled',
|
||||
},
|
||||
]}
|
||||
isEdited={ffmpegConfig.accel !== savedConfig.accel}
|
||||
/>
|
||||
and
|
||||
<a href="https://trac.ffmpeg.org/wiki/Encode/VP9" class="underline" target="_blank" rel="noreferrer"
|
||||
>VP9 codec</a
|
||||
>.
|
||||
</p>
|
||||
|
||||
<SettingSelect
|
||||
label="CONSTANT QUALITY MODE"
|
||||
desc="ICQ is better than CQP, but some hardware acceleration devices do not support this mode. Setting this option will prefer the specified mode when using quality-based encoding. Ignored by NVENC as it does not support ICQ."
|
||||
bind:value={ffmpegConfig.cqMode}
|
||||
options={[
|
||||
{ value: CQMode.Auto, text: 'Auto' },
|
||||
{ value: CQMode.Icq, text: 'ICQ' },
|
||||
{ value: CQMode.Cqp, text: 'CQP' },
|
||||
]}
|
||||
isEdited={ffmpegConfig.cqMode !== savedConfig.cqMode}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
{disabled}
|
||||
label="CONSTANT RATE FACTOR (-crf)"
|
||||
desc="Video quality level. Typical values are 23 for H.264, 28 for HEVC, and 31 for VP9. Lower is better, but takes longer to encode and produces larger files."
|
||||
bind:value={config.ffmpeg.crf}
|
||||
required={true}
|
||||
isEdited={config.ffmpeg.crf !== savedConfig.ffmpeg.crf}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="TEMPORAL AQ"
|
||||
{disabled}
|
||||
subtitle="Applies only to NVENC. Increases quality of high-detail, low-motion scenes. May not be compatible with older devices."
|
||||
bind:checked={ffmpegConfig.temporalAQ}
|
||||
isEdited={ffmpegConfig.temporalAQ !== savedConfig.temporalAQ}
|
||||
/>
|
||||
</div>
|
||||
</SettingAccordion>
|
||||
<SettingSelect
|
||||
label="PRESET (-preset)"
|
||||
{disabled}
|
||||
desc="Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above `faster`."
|
||||
bind:value={config.ffmpeg.preset}
|
||||
name="preset"
|
||||
options={[
|
||||
{ value: 'ultrafast', text: 'ultrafast' },
|
||||
{ value: 'superfast', text: 'superfast' },
|
||||
{ value: 'veryfast', text: 'veryfast' },
|
||||
{ value: 'faster', text: 'faster' },
|
||||
{ value: 'fast', text: 'fast' },
|
||||
{ value: 'medium', text: 'medium' },
|
||||
{ value: 'slow', text: 'slow' },
|
||||
{ value: 'slower', text: 'slower' },
|
||||
{ value: 'veryslow', text: 'veryslow' },
|
||||
]}
|
||||
isEdited={config.ffmpeg.preset !== savedConfig.ffmpeg.preset}
|
||||
/>
|
||||
|
||||
<SettingAccordion title="Advanced" subtitle="Options most users should not need to change">
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="TONE-MAPPING NPL"
|
||||
desc="Colors will be adjusted to look normal for a display of this brightness. Counter-intuitively, lower values increase the brightness of the video and vice versa since it compensates for the brightness of the display. 0 sets this value automatically."
|
||||
bind:value={ffmpegConfig.npl}
|
||||
isEdited={ffmpegConfig.npl !== savedConfig.npl}
|
||||
/>
|
||||
<SettingSelect
|
||||
label="AUDIO CODEC"
|
||||
{disabled}
|
||||
desc="Opus is the highest quality option, but has lower compatibility with old devices or software."
|
||||
bind:value={config.ffmpeg.targetAudioCodec}
|
||||
options={[
|
||||
{ value: AudioCodec.Aac, text: 'aac' },
|
||||
{ value: AudioCodec.Mp3, text: 'mp3' },
|
||||
{ value: AudioCodec.Libopus, text: 'opus' },
|
||||
]}
|
||||
name="acodec"
|
||||
isEdited={config.ffmpeg.targetAudioCodec !== savedConfig.ffmpeg.targetAudioCodec}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="MAX B-FRAMES"
|
||||
desc="Higher values improve compression efficiency, but slow down encoding. May not be compatible with hardware acceleration on older devices. 0 disables B-frames, while -1 sets this value automatically."
|
||||
bind:value={ffmpegConfig.bframes}
|
||||
isEdited={ffmpegConfig.bframes !== savedConfig.bframes}
|
||||
/>
|
||||
<SettingSelect
|
||||
label="VIDEO CODEC"
|
||||
{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."
|
||||
bind:value={config.ffmpeg.targetVideoCodec}
|
||||
options={[
|
||||
{ value: VideoCodec.H264, text: 'h264' },
|
||||
{ value: VideoCodec.Hevc, text: 'hevc' },
|
||||
{ value: VideoCodec.Vp9, text: 'vp9' },
|
||||
]}
|
||||
name="vcodec"
|
||||
isEdited={config.ffmpeg.targetVideoCodec !== savedConfig.ffmpeg.targetVideoCodec}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="REFERENCE FRAMES"
|
||||
desc="The number of frames to reference when compressing a given frame. Higher values improve compression efficiency, but slow down encoding. 0 sets this value automatically."
|
||||
bind:value={ffmpegConfig.refs}
|
||||
isEdited={ffmpegConfig.refs !== savedConfig.refs}
|
||||
/>
|
||||
<SettingSelect
|
||||
label="TARGET RESOLUTION"
|
||||
{disabled}
|
||||
desc="Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
||||
bind:value={config.ffmpeg.targetResolution}
|
||||
options={[
|
||||
{ value: '2160', text: '4k' },
|
||||
{ value: '1440', text: '1440p' },
|
||||
{ value: '1080', text: '1080p' },
|
||||
{ value: '720', text: '720p' },
|
||||
{ value: '480', text: '480p' },
|
||||
{ value: 'original', text: 'original' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={config.ffmpeg.targetResolution !== savedConfig.ffmpeg.targetResolution}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="MAX KEYFRAME INTERVAL"
|
||||
desc="Sets the maximum frame distance between keyframes. Lower values worsen compression efficiency, but improve seek times and may improve quality in scenes with fast movement. 0 sets this value automatically."
|
||||
bind:value={ffmpegConfig.gopSize}
|
||||
isEdited={ffmpegConfig.gopSize !== savedConfig.gopSize}
|
||||
/>
|
||||
</div>
|
||||
</SettingAccordion>
|
||||
</div>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
{disabled}
|
||||
label="MAX BITRATE"
|
||||
desc="Setting a max bitrate can make file sizes more predictable at a minor cost to quality. At 720p, typical values are 2600k for VP9 or HEVC, or 4500k for H.264. Disabled if set to 0."
|
||||
bind:value={config.ffmpeg.maxBitrate}
|
||||
isEdited={config.ffmpeg.maxBitrate !== savedConfig.ffmpeg.maxBitrate}
|
||||
/>
|
||||
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => handleReset(detail)}
|
||||
on:save={saveSetting}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
{disabled}
|
||||
label="THREADS"
|
||||
desc="Higher values lead to faster encoding, but leave less room for the server to process other tasks while active. This value should not be more than the number of CPU cores. Maximizes utilization if set to 0."
|
||||
bind:value={config.ffmpeg.threads}
|
||||
isEdited={config.ffmpeg.threads !== savedConfig.ffmpeg.threads}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="TRANSCODE POLICY"
|
||||
{disabled}
|
||||
desc="Policy for when a video should be transcoded."
|
||||
bind:value={config.ffmpeg.transcode}
|
||||
name="transcode"
|
||||
options={[
|
||||
{ value: TranscodePolicy.All, text: 'All videos' },
|
||||
{
|
||||
value: TranscodePolicy.Optimal,
|
||||
text: 'Videos higher than target resolution or not in the desired format',
|
||||
},
|
||||
{
|
||||
value: TranscodePolicy.Required,
|
||||
text: 'Only videos not in the desired format',
|
||||
},
|
||||
{
|
||||
value: TranscodePolicy.Disabled,
|
||||
text: "Don't transcode any videos, may break playback on some clients",
|
||||
},
|
||||
]}
|
||||
isEdited={config.ffmpeg.transcode !== savedConfig.ffmpeg.transcode}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="TONE-MAPPING"
|
||||
{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."
|
||||
bind:value={config.ffmpeg.tonemap}
|
||||
name="tonemap"
|
||||
options={[
|
||||
{
|
||||
value: ToneMapping.Hable,
|
||||
text: 'Hable',
|
||||
},
|
||||
{
|
||||
value: ToneMapping.Mobius,
|
||||
text: 'Mobius',
|
||||
},
|
||||
{
|
||||
value: ToneMapping.Reinhard,
|
||||
text: 'Reinhard',
|
||||
},
|
||||
{
|
||||
value: ToneMapping.Disabled,
|
||||
text: 'Disabled',
|
||||
},
|
||||
]}
|
||||
isEdited={config.ffmpeg.tonemap !== savedConfig.ffmpeg.tonemap}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="TWO-PASS ENCODING"
|
||||
{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={config.ffmpeg.twoPass}
|
||||
isEdited={config.ffmpeg.twoPass !== savedConfig.ffmpeg.twoPass}
|
||||
/>
|
||||
|
||||
<SettingAccordion
|
||||
title="Hardware Acceleration"
|
||||
subtitle="Experimental; much faster, but will have lower quality at the same bitrate"
|
||||
>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSelect
|
||||
label="ACCELERATION API"
|
||||
{disabled}
|
||||
desc="The API that will interact with your device to accelerate transcoding. This setting is 'best effort': it will fallback to software transcoding on failure. VP9 may or may not work depending on your hardware."
|
||||
bind:value={config.ffmpeg.accel}
|
||||
name="accel"
|
||||
options={[
|
||||
{ value: TranscodeHWAccel.Nvenc, text: 'NVENC (requires NVIDIA GPU)' },
|
||||
{
|
||||
value: TranscodeHWAccel.Qsv,
|
||||
text: 'Quick Sync (requires 7th gen Intel CPU or later)',
|
||||
},
|
||||
{
|
||||
value: TranscodeHWAccel.Vaapi,
|
||||
text: 'VAAPI',
|
||||
},
|
||||
{
|
||||
value: TranscodeHWAccel.Rkmpp,
|
||||
text: 'RKMPP (only on Rockchip SOCs)',
|
||||
},
|
||||
{
|
||||
value: TranscodeHWAccel.Disabled,
|
||||
text: 'Disabled',
|
||||
},
|
||||
]}
|
||||
isEdited={config.ffmpeg.accel !== savedConfig.ffmpeg.accel}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="CONSTANT QUALITY MODE"
|
||||
desc="ICQ is better than CQP, but some hardware acceleration devices do not support this mode. Setting this option will prefer the specified mode when using quality-based encoding. Ignored by NVENC as it does not support ICQ."
|
||||
bind:value={config.ffmpeg.cqMode}
|
||||
options={[
|
||||
{ value: CQMode.Auto, text: 'Auto' },
|
||||
{ value: CQMode.Icq, text: 'ICQ' },
|
||||
{ value: CQMode.Cqp, text: 'CQP' },
|
||||
]}
|
||||
isEdited={config.ffmpeg.cqMode !== savedConfig.ffmpeg.cqMode}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="TEMPORAL AQ"
|
||||
{disabled}
|
||||
subtitle="Applies only to NVENC. Increases quality of high-detail, low-motion scenes. May not be compatible with older devices."
|
||||
bind:checked={config.ffmpeg.temporalAQ}
|
||||
isEdited={config.ffmpeg.temporalAQ !== savedConfig.ffmpeg.temporalAQ}
|
||||
/>
|
||||
</div>
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Advanced" subtitle="Options most users should not need to change">
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="TONE-MAPPING NPL"
|
||||
desc="Colors will be adjusted to look normal for a display of this brightness. Counter-intuitively, lower values increase the brightness of the video and vice versa since it compensates for the brightness of the display. 0 sets this value automatically."
|
||||
bind:value={config.ffmpeg.npl}
|
||||
isEdited={config.ffmpeg.npl !== savedConfig.ffmpeg.npl}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="MAX B-FRAMES"
|
||||
desc="Higher values improve compression efficiency, but slow down encoding. May not be compatible with hardware acceleration on older devices. 0 disables B-frames, while -1 sets this value automatically."
|
||||
bind:value={config.ffmpeg.bframes}
|
||||
isEdited={config.ffmpeg.bframes !== savedConfig.ffmpeg.bframes}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="REFERENCE FRAMES"
|
||||
desc="The number of frames to reference when compressing a given frame. Higher values improve compression efficiency, but slow down encoding. 0 sets this value automatically."
|
||||
bind:value={config.ffmpeg.refs}
|
||||
isEdited={config.ffmpeg.refs !== savedConfig.ffmpeg.refs}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="MAX KEYFRAME INTERVAL"
|
||||
desc="Sets the maximum frame distance between keyframes. Lower values worsen compression efficiency, but improve seek times and may improve quality in scenes with fast movement. 0 sets this value automatically."
|
||||
bind:value={config.ffmpeg.gopSize}
|
||||
isEdited={config.ffmpeg.gopSize !== savedConfig.ffmpeg.gopSize}
|
||||
/>
|
||||
</div>
|
||||
</SettingAccordion>
|
||||
</div>
|
||||
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['ffmpeg'] })}
|
||||
on:save={() => dispatch('save', { ffmpeg: config.ffmpeg })}
|
||||
showResetToDefault={!isEqual(savedConfig.ffmpeg, defaultConfig)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,21 +1,18 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { api, JobName, SystemConfigJobDto } from '@api';
|
||||
import { api, JobName, SystemConfigDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { handleError } from '../../../../utils/handle-error';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
import type { ResetOptions } from '$lib/utils/dipatch';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { SettingsEventType } from '../admin-settings';
|
||||
|
||||
export let jobConfig: SystemConfigJobDto; // this is the config that is being edited
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
export let config: SystemConfigDto; // this is the config that is being edited
|
||||
export let disabled = false;
|
||||
|
||||
let savedConfig: SystemConfigJobDto;
|
||||
let defaultConfig: SystemConfigJobDto;
|
||||
const dispatch = createEventDispatcher<SettingsEventType>();
|
||||
|
||||
const jobNames = [
|
||||
JobName.ThumbnailGeneration,
|
||||
@ -27,94 +24,33 @@
|
||||
JobName.VideoConversion,
|
||||
JobName.Migration,
|
||||
];
|
||||
|
||||
const handleReset = (detail: ResetOptions) => {
|
||||
if (detail.default) {
|
||||
resetToDefault();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.job),
|
||||
api.systemConfigApi.getConfigDefaults().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() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
jobConfig = { ...resetConfig.job };
|
||||
savedConfig = { ...resetConfig.job };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset Job settings to the recent saved settings',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
|
||||
|
||||
jobConfig = { ...configs.job };
|
||||
defaultConfig = { ...configs.job };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset Job settings to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
{#each jobNames as jobName}
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingInputField
|
||||
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={({ detail }) => handleReset(detail)}
|
||||
on:save={saveSetting}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
{#each jobNames as jobName}
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
{disabled}
|
||||
label="{api.getJobName(jobName)} Concurrency"
|
||||
desc=""
|
||||
bind:value={config.job[jobName].concurrency}
|
||||
required={true}
|
||||
isEdited={!(config.job[jobName].concurrency == savedConfig.job[jobName].concurrency)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
{/each}
|
||||
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['job'] })}
|
||||
on:save={() => dispatch('save', { job: config.job })}
|
||||
showResetToDefault={!isEqual(savedConfig.job, defaultConfig.job)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,19 +1,17 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { api, SystemConfigLibraryDto } from '@api';
|
||||
import type { SystemConfigDto } from '@api';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { handleError } from '../../../../utils/handle-error';
|
||||
import SettingAccordion from '../setting-accordion.svelte';
|
||||
import type { ResetOptions } from '$lib/utils/dipatch';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { SettingsEventType } from '../admin-settings';
|
||||
|
||||
export let libraryConfig: SystemConfigLibraryDto; // this is the config that is being edited
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
export let config: SystemConfigDto; // this is the config that is being edited
|
||||
export let disabled = false;
|
||||
|
||||
const cronExpressionOptions = [
|
||||
@ -23,131 +21,66 @@
|
||||
{ title: 'Every 6 hours', expression: '0 */6 * * *' },
|
||||
];
|
||||
|
||||
let savedConfig: SystemConfigLibraryDto;
|
||||
let defaultConfig: SystemConfigLibraryDto;
|
||||
|
||||
const handleReset = (detail: ResetOptions) => {
|
||||
if (detail.default) {
|
||||
resetToDefault();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.library),
|
||||
api.systemConfigApi.getConfigDefaults().then((res) => res.data.library),
|
||||
]);
|
||||
}
|
||||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: configs } = await api.systemConfigApi.getConfig();
|
||||
|
||||
const result = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...configs,
|
||||
library: libraryConfig,
|
||||
},
|
||||
});
|
||||
|
||||
libraryConfig = { ...result.data.library };
|
||||
savedConfig = { ...result.data.library };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Library settings saved',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (e) {
|
||||
handleError(e, 'Unable to save settings');
|
||||
}
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
libraryConfig = { ...resetConfig.library };
|
||||
savedConfig = { ...resetConfig.library };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset library settings to the recent saved settings',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
|
||||
|
||||
libraryConfig = { ...configs.library };
|
||||
defaultConfig = { ...configs.library };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset library settings to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
const dispatch = createEventDispatcher<SettingsEventType>();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<SettingAccordion title="Scanning" subtitle="Settings for library scanning" isOpen>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
{disabled}
|
||||
subtitle="Enable automatic library scanning"
|
||||
bind:checked={libraryConfig.scan.enabled}
|
||||
/>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<SettingAccordion title="Scanning" subtitle="Settings for library scanning" isOpen>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
{disabled}
|
||||
subtitle="Enable automatic library scanning"
|
||||
bind:checked={config.library.scan.enabled}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col my-2 dark:text-immich-dark-fg">
|
||||
<label class="text-sm" for="expression-select">Cron Expression Presets</label>
|
||||
<select
|
||||
class="p-2 mt-2 text-sm rounded-lg bg-slate-200 hover:cursor-pointer dark:bg-gray-600"
|
||||
disabled={disabled || !libraryConfig.scan.enabled}
|
||||
name="expression"
|
||||
id="expression-select"
|
||||
bind:value={libraryConfig.scan.cronExpression}
|
||||
>
|
||||
{#each cronExpressionOptions as { title, expression }}
|
||||
<option value={expression}>{title}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
required={true}
|
||||
disabled={disabled || !libraryConfig.scan.enabled}
|
||||
label="Cron Expression"
|
||||
bind:value={libraryConfig.scan.cronExpression}
|
||||
isEdited={libraryConfig.scan.cronExpression !== savedConfig.scan.cronExpression}
|
||||
<div class="flex flex-col my-2 dark:text-immich-dark-fg">
|
||||
<label class="text-sm" for="expression-select">Cron Expression Presets</label>
|
||||
<select
|
||||
class="p-2 mt-2 text-sm rounded-lg bg-slate-200 hover:cursor-pointer dark:bg-gray-600"
|
||||
disabled={disabled || !config.library.scan.enabled}
|
||||
name="expression"
|
||||
id="expression-select"
|
||||
bind:value={config.library.scan.cronExpression}
|
||||
>
|
||||
<svelte:fragment slot="desc">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
Set the scanning interval using the cron format. For more information please refer to e.g. <a
|
||||
href="https://crontab.guru"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer">Crontab Guru</a
|
||||
>
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</SettingInputField>
|
||||
{#each cronExpressionOptions as { title, expression }}
|
||||
<option value={expression}>{title}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => handleReset(detail)}
|
||||
on:save={saveSetting}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</SettingAccordion>
|
||||
</div>
|
||||
{/await}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
required={true}
|
||||
disabled={disabled || !config.library.scan.enabled}
|
||||
label="Cron Expression"
|
||||
bind:value={config.library.scan.cronExpression}
|
||||
isEdited={config.library.scan.cronExpression !== savedConfig.library.scan.cronExpression}
|
||||
>
|
||||
<svelte:fragment slot="desc">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
Set the scanning interval using the cron format. For more information please refer to e.g. <a
|
||||
href="https://crontab.guru"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer">Crontab Guru</a
|
||||
>
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
</SettingInputField>
|
||||
</div>
|
||||
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['library'] })}
|
||||
on:save={() => dispatch('save', { library: config.library })}
|
||||
showResetToDefault={!isEqual(savedConfig.library, defaultConfig.library)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</SettingAccordion>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,118 +1,50 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, LogLevel, SystemConfigLoggingDto } from '@api';
|
||||
import { LogLevel, SystemConfigDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
import SettingSelect from '../setting-select.svelte';
|
||||
import type { ResetOptions } from '$lib/utils/dipatch';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { SettingsEventType } from '../admin-settings';
|
||||
|
||||
export let loggingConfig: SystemConfigLoggingDto; // this is the config that is being edited
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
export let config: SystemConfigDto; // this is the config that is being edited
|
||||
export let disabled = false;
|
||||
|
||||
let savedConfig: SystemConfigLoggingDto;
|
||||
let defaultConfig: SystemConfigLoggingDto;
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.logging),
|
||||
api.systemConfigApi.getConfigDefaults().then((res) => res.data.logging),
|
||||
]);
|
||||
}
|
||||
|
||||
const handleReset = (detail: ResetOptions) => {
|
||||
if (detail.default) {
|
||||
resetToDefault();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: current } = await api.systemConfigApi.getConfig();
|
||||
const { data: updated } = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...current,
|
||||
logging: loggingConfig,
|
||||
},
|
||||
});
|
||||
|
||||
loggingConfig = { ...updated.logging };
|
||||
savedConfig = { ...updated.logging };
|
||||
|
||||
notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save settings');
|
||||
}
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
loggingConfig = { ...resetConfig.logging };
|
||||
savedConfig = { ...resetConfig.logging };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset settings to the recent saved settings',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
|
||||
|
||||
loggingConfig = { ...configs.logging };
|
||||
defaultConfig = { ...configs.logging };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset password settings to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
const dispatch = createEventDispatcher<SettingsEventType>();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ml-4">
|
||||
<SettingSwitch title="ENABLED" {disabled} subtitle="Logging" bind:checked={loggingConfig.enabled} />
|
||||
</div>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch title="ENABLED" {disabled} subtitle="Logging" bind:checked={config.logging.enabled} />
|
||||
<SettingSelect
|
||||
label="LEVEL"
|
||||
desc="When enabled, what log level to use."
|
||||
bind:value={config.logging.level}
|
||||
options={[
|
||||
{ value: LogLevel.Fatal, text: 'Fatal' },
|
||||
{ value: LogLevel.Error, text: 'Error' },
|
||||
{ value: LogLevel.Warn, text: 'Warn' },
|
||||
{ value: LogLevel.Log, text: 'Log' },
|
||||
{ value: LogLevel.Debug, text: 'Debug' },
|
||||
{ value: LogLevel.Verbose, text: 'Verbose' },
|
||||
]}
|
||||
name="level"
|
||||
isEdited={config.logging.level !== savedConfig.logging.level}
|
||||
disabled={disabled || !config.logging.enabled}
|
||||
/>
|
||||
|
||||
<div class="ml-4">
|
||||
<SettingSelect
|
||||
label="LEVEL"
|
||||
desc="When enabled, what log level to use."
|
||||
bind:value={loggingConfig.level}
|
||||
options={[
|
||||
{ value: LogLevel.Fatal, text: 'Fatal' },
|
||||
{ value: LogLevel.Error, text: 'Error' },
|
||||
{ value: LogLevel.Warn, text: 'Warn' },
|
||||
{ value: LogLevel.Log, text: 'Log' },
|
||||
{ value: LogLevel.Debug, text: 'Debug' },
|
||||
{ value: LogLevel.Verbose, text: 'Verbose' },
|
||||
]}
|
||||
name="level"
|
||||
isEdited={loggingConfig.level !== savedConfig.level}
|
||||
disabled={disabled || !loggingConfig.enabled}
|
||||
/>
|
||||
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => handleReset(detail)}
|
||||
on:save={saveSetting}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['logging'] })}
|
||||
on:save={() => dispatch('save', { logging: config.logging })}
|
||||
showResetToDefault={!isEqual(savedConfig.logging, defaultConfig.logging)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,10 +1,5 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, SystemConfigMachineLearningDto } from '@api';
|
||||
import type { SystemConfigDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
@ -12,181 +7,141 @@
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
import SettingAccordion from '../setting-accordion.svelte';
|
||||
import SettingSelect from '../setting-select.svelte';
|
||||
import type { ResetOptions } from '$lib/utils/dipatch';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { SettingsEventType } from '../admin-settings';
|
||||
|
||||
export let machineLearningConfig: SystemConfigMachineLearningDto; // this is the config that is being edited
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
export let config: SystemConfigDto; // this is the config that is being edited
|
||||
export let disabled = false;
|
||||
|
||||
let savedConfig: SystemConfigMachineLearningDto;
|
||||
let defaultConfig: SystemConfigMachineLearningDto;
|
||||
|
||||
async function refreshConfig() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.machineLearning),
|
||||
api.systemConfigApi.getConfigDefaults().then((res) => res.data.machineLearning),
|
||||
]);
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
machineLearningConfig = { ...resetConfig.machineLearning };
|
||||
savedConfig = { ...resetConfig.machineLearning };
|
||||
notificationController.show({ message: 'Reset to the last saved settings', type: NotificationType.Info });
|
||||
}
|
||||
|
||||
const handleReset = (detail: ResetOptions) => {
|
||||
if (detail.default) {
|
||||
resetToDefault();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
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() {
|
||||
machineLearningConfig = { ...defaultConfig };
|
||||
notificationController.show({ message: 'Reset settings to defaults', type: NotificationType.Info });
|
||||
}
|
||||
const dispatch = createEventDispatcher<SettingsEventType>();
|
||||
</script>
|
||||
|
||||
<div class="mt-2">
|
||||
{#await refreshConfig() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault class="mx-4 mt-4">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault class="mx-4 mt-4">
|
||||
<div class="flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
subtitle="If disabled, all ML features will be disabled regardless of the below settings."
|
||||
{disabled}
|
||||
bind:checked={config.machineLearning.enabled}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="URL"
|
||||
desc="URL of the machine learning server"
|
||||
bind:value={config.machineLearning.url}
|
||||
required={true}
|
||||
disabled={disabled || !config.machineLearning.enabled}
|
||||
isEdited={config.machineLearning.url !== savedConfig.machineLearning.url}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<SettingAccordion title="Smart Search" subtitle="Search for images semantically using CLIP embeddings">
|
||||
<div class="ml-4 mt-4 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}
|
||||
subtitle="If disabled, images will not be encoded for smart search."
|
||||
bind:checked={config.machineLearning.clip.enabled}
|
||||
disabled={disabled || !config.machineLearning.enabled}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="URL"
|
||||
desc="URL of the machine learning server"
|
||||
bind:value={machineLearningConfig.url}
|
||||
label="CLIP MODEL"
|
||||
bind:value={config.machineLearning.clip.modelName}
|
||||
required={true}
|
||||
disabled={disabled || !machineLearningConfig.enabled}
|
||||
isEdited={machineLearningConfig.url !== savedConfig.url}
|
||||
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.clip.enabled}
|
||||
isEdited={config.machineLearning.clip.modelName !== savedConfig.machineLearning.clip.modelName}
|
||||
>
|
||||
<p slot="desc" class="immich-form-label pb-2 text-sm">
|
||||
The name of a CLIP model listed <a href="https://huggingface.co/immich-app"><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">
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
subtitle="If disabled, images will not be encoded for facial recognition and will not populate the People section in the Explore page."
|
||||
bind:checked={config.machineLearning.facialRecognition.enabled}
|
||||
disabled={disabled || !config.machineLearning.enabled}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<SettingSelect
|
||||
label="FACIAL RECOGNITION MODEL"
|
||||
desc="Models are listed in descending order of size. Larger models are slower and use more memory, but produce better results. Note that you must re-run the Recognize Faces job for all images upon changing a model."
|
||||
name="facial-recognition-model"
|
||||
bind:value={config.machineLearning.facialRecognition.modelName}
|
||||
options={[
|
||||
{ value: 'antelopev2', text: 'antelopev2' },
|
||||
{ value: 'buffalo_l', text: 'buffalo_l' },
|
||||
{ value: 'buffalo_m', text: 'buffalo_m' },
|
||||
{ value: 'buffalo_s', text: 'buffalo_s' },
|
||||
]}
|
||||
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled}
|
||||
isEdited={config.machineLearning.facialRecognition.modelName !==
|
||||
savedConfig.machineLearning.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={config.machineLearning.facialRecognition.minScore}
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="1"
|
||||
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled}
|
||||
isEdited={config.machineLearning.facialRecognition.minScore !==
|
||||
savedConfig.machineLearning.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={config.machineLearning.facialRecognition.maxDistance}
|
||||
step="0.1"
|
||||
min="0"
|
||||
max="2"
|
||||
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled}
|
||||
isEdited={config.machineLearning.facialRecognition.maxDistance !==
|
||||
savedConfig.machineLearning.facialRecognition.maxDistance}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="MIN FACES DETECTED"
|
||||
desc="The minimum number of faces of a person that must be detected for them to appear in the People tab. Setting this to a value greater than 1 can prevent strangers or blurry faces that are not the main subject of the image from being displayed."
|
||||
bind:value={config.machineLearning.facialRecognition.minFaces}
|
||||
step="1"
|
||||
min="1"
|
||||
disabled={disabled || !config.machineLearning.enabled || !config.machineLearning.facialRecognition.enabled}
|
||||
isEdited={config.machineLearning.facialRecognition.minFaces !==
|
||||
savedConfig.machineLearning.facialRecognition.minFaces}
|
||||
/>
|
||||
</div>
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Smart Search" subtitle="Search for images semantically using CLIP embeddings">
|
||||
<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 />
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="CLIP MODEL"
|
||||
bind:value={machineLearningConfig.clip.modelName}
|
||||
required={true}
|
||||
disabled={disabled || !machineLearningConfig.enabled || !machineLearningConfig.clip.enabled}
|
||||
isEdited={machineLearningConfig.clip.modelName !== savedConfig.clip.modelName}
|
||||
>
|
||||
<p slot="desc" class="immich-form-label pb-2 text-sm">
|
||||
The name of a CLIP model listed <a href="https://huggingface.co/immich-app"><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">
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
subtitle="If disabled, images will not be encoded for facial recognition and will not populate the People section in the Explore page."
|
||||
bind:checked={machineLearningConfig.facialRecognition.enabled}
|
||||
disabled={disabled || !machineLearningConfig.enabled}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
|
||||
<SettingSelect
|
||||
label="FACIAL RECOGNITION MODEL"
|
||||
desc="Models are listed in descending order of size. Larger models are slower and use more memory, but produce better results. Note that you must re-run the Recognize Faces job for all images upon changing a model."
|
||||
name="facial-recognition-model"
|
||||
bind:value={machineLearningConfig.facialRecognition.modelName}
|
||||
options={[
|
||||
{ value: 'antelopev2', text: 'antelopev2' },
|
||||
{ value: 'buffalo_l', text: 'buffalo_l' },
|
||||
{ value: 'buffalo_m', text: 'buffalo_m' },
|
||||
{ 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}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="MIN FACES DETECTED"
|
||||
desc="The minimum number of faces of a person that must be detected for them to appear in the People tab. Setting this to a value greater than 1 can prevent strangers or blurry faces that are not the main subject of the image from being displayed."
|
||||
bind:value={machineLearningConfig.facialRecognition.minFaces}
|
||||
step="1"
|
||||
min="1"
|
||||
disabled={disabled || !machineLearningConfig.enabled || !machineLearningConfig.facialRecognition.enabled}
|
||||
isEdited={machineLearningConfig.facialRecognition.minFaces !== savedConfig.facialRecognition.minFaces}
|
||||
/>
|
||||
</div>
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => handleReset(detail)}
|
||||
on:save={saveSetting}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
{disabled}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['machineLearning'] })}
|
||||
on:save={() => dispatch('save', { machineLearning: config.machineLearning })}
|
||||
showResetToDefault={!isEqual(savedConfig.machineLearning, defaultConfig.machineLearning)}
|
||||
{disabled}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,157 +1,87 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, SystemConfigDto } from '@api';
|
||||
import { cloneDeep, isEqual } from 'lodash-es';
|
||||
import type { SystemConfigDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import SettingAccordion from '../setting-accordion.svelte';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
import type { ResetOptions } from '$lib/utils/dipatch';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { SettingsEventType } from '../admin-settings';
|
||||
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
export let config: SystemConfigDto; // this is the config that is being edited
|
||||
export let disabled = false;
|
||||
|
||||
let savedConfig: SystemConfigDto;
|
||||
let defaultConfig: SystemConfigDto;
|
||||
|
||||
const handleReset = (detail: ResetOptions) => {
|
||||
if (detail.default) {
|
||||
resetToDefault();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
async function refreshConfig() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data),
|
||||
api.systemConfigApi.getConfigDefaults().then((res) => res.data),
|
||||
]);
|
||||
}
|
||||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: current } = await api.systemConfigApi.getConfig();
|
||||
const { data: updated } = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...current,
|
||||
map: {
|
||||
enabled: config.map.enabled,
|
||||
lightStyle: config.map.lightStyle,
|
||||
darkStyle: config.map.darkStyle,
|
||||
},
|
||||
reverseGeocoding: {
|
||||
enabled: config.reverseGeocoding.enabled,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
config = cloneDeep(updated);
|
||||
savedConfig = cloneDeep(updated);
|
||||
|
||||
notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save settings');
|
||||
}
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
config = cloneDeep(resetConfig);
|
||||
savedConfig = cloneDeep(resetConfig);
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset settings to the recent saved settings',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
|
||||
|
||||
config = cloneDeep(configs);
|
||||
defaultConfig = cloneDeep(configs);
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset map settings to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
const dispatch = createEventDispatcher<SettingsEventType>();
|
||||
</script>
|
||||
|
||||
<div class="mt-2">
|
||||
{#await refreshConfig() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="flex flex-col gap-4">
|
||||
<SettingAccordion title="Map Settings" subtitle="Manage map settings">
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
{disabled}
|
||||
subtitle="Enable map features"
|
||||
bind:checked={config.map.enabled}
|
||||
/>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="flex flex-col gap-4">
|
||||
<SettingAccordion title="Map Settings" subtitle="Manage map settings">
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
{disabled}
|
||||
subtitle="Enable map features"
|
||||
bind:checked={config.map.enabled}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
<hr />
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="Light Style"
|
||||
desc="URL to a style.json map theme"
|
||||
bind:value={config.map.lightStyle}
|
||||
disabled={disabled || !config.map.enabled}
|
||||
isEdited={config.map.lightStyle !== savedConfig.map.lightStyle}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="Dark Style"
|
||||
desc="URL to a style.json map theme"
|
||||
bind:value={config.map.darkStyle}
|
||||
disabled={disabled || !config.map.enabled}
|
||||
isEdited={config.map.darkStyle !== savedConfig.map.darkStyle}
|
||||
/>
|
||||
</div></SettingAccordion
|
||||
>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="Light Style"
|
||||
desc="URL to a style.json map theme"
|
||||
bind:value={config.map.lightStyle}
|
||||
disabled={disabled || !config.map.enabled}
|
||||
isEdited={config.map.lightStyle !== savedConfig.map.lightStyle}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="Dark Style"
|
||||
desc="URL to a style.json map theme"
|
||||
bind:value={config.map.darkStyle}
|
||||
disabled={disabled || !config.map.enabled}
|
||||
isEdited={config.map.darkStyle !== savedConfig.map.darkStyle}
|
||||
/>
|
||||
</div></SettingAccordion
|
||||
>
|
||||
|
||||
<SettingAccordion title="Reverse Geocoding Settings">
|
||||
<svelte:fragment slot="subtitle">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
Manage <a
|
||||
href="https://immich.app/docs/features/reverse-geocoding"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer">Reverse Geocoding</a
|
||||
> settings
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
{disabled}
|
||||
subtitle="Enable reverse geocoding"
|
||||
bind:checked={config.reverseGeocoding.enabled}
|
||||
/>
|
||||
</div></SettingAccordion
|
||||
>
|
||||
<SettingAccordion title="Reverse Geocoding Settings">
|
||||
<svelte:fragment slot="subtitle">
|
||||
<p class="text-sm dark:text-immich-dark-fg">
|
||||
Manage <a
|
||||
href="https://immich.app/docs/features/reverse-geocoding"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer">Reverse Geocoding</a
|
||||
> settings
|
||||
</p>
|
||||
</svelte:fragment>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
{disabled}
|
||||
subtitle="Enable reverse geocoding"
|
||||
bind:checked={config.reverseGeocoding.enabled}
|
||||
/>
|
||||
</div></SettingAccordion
|
||||
>
|
||||
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => handleReset(detail)}
|
||||
on:save={saveSetting}
|
||||
showResetToDefault={!isEqual(
|
||||
{ ...savedConfig.map, ...savedConfig.reverseGeocoding },
|
||||
{ ...defaultConfig.map, ...defaultConfig.reverseGeocoding },
|
||||
)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['map', 'reverseGeocoding'] })}
|
||||
on:save={() => dispatch('save', { map: config.map, reverseGeocoding: config.reverseGeocoding })}
|
||||
showResetToDefault={!isEqual(
|
||||
{ map: savedConfig.map, reverseGeocoding: savedConfig.reverseGeocoding },
|
||||
{ map: defaultConfig.map, reverseGeocoding: defaultConfig.reverseGeocoding },
|
||||
)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,103 +1,37 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, SystemConfigNewVersionCheckDto } from '@api';
|
||||
import type { SystemConfigDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
import type { ResetOptions } from '$lib/utils/dipatch';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { SettingsEventType } from '../admin-settings';
|
||||
|
||||
export let newVersionCheckConfig: SystemConfigNewVersionCheckDto; // this is the config that is being edited
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
export let config: SystemConfigDto; // this is the config that is being edited
|
||||
export let disabled = false;
|
||||
|
||||
let savedConfig: SystemConfigNewVersionCheckDto;
|
||||
let defaultConfig: SystemConfigNewVersionCheckDto;
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.newVersionCheck),
|
||||
api.systemConfigApi.getConfigDefaults().then((res) => res.data.newVersionCheck),
|
||||
]);
|
||||
}
|
||||
|
||||
const handleReset = (detail: ResetOptions) => {
|
||||
if (detail.default) {
|
||||
resetToDefault();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: configs } = await api.systemConfigApi.getConfig();
|
||||
|
||||
const result = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...configs,
|
||||
newVersionCheck: newVersionCheckConfig,
|
||||
},
|
||||
});
|
||||
|
||||
newVersionCheckConfig = { ...result.data.newVersionCheck };
|
||||
savedConfig = { ...result.data.newVersionCheck };
|
||||
|
||||
notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save settings');
|
||||
}
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
newVersionCheckConfig = { ...resetConfig.newVersionCheck };
|
||||
savedConfig = { ...resetConfig.newVersionCheck };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset settings to the recent saved settings',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
|
||||
|
||||
newVersionCheckConfig = { ...configs.newVersionCheck };
|
||||
defaultConfig = { ...configs.newVersionCheck };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset settings to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
const dispatch = createEventDispatcher<SettingsEventType>();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ml-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
subtitle="Enable period requests to GitHub to check for new releases"
|
||||
bind:checked={newVersionCheckConfig.enabled}
|
||||
{disabled}
|
||||
/>
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => handleReset(detail)}
|
||||
on:save={saveSetting}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
subtitle="Enable period requests to GitHub to check for new releases"
|
||||
bind:checked={config.newVersionCheck.enabled}
|
||||
{disabled}
|
||||
/>
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['newVersionCheck'] })}
|
||||
on:save={() => dispatch('save', { newVersionCheck: config.newVersionCheck })}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,59 +1,29 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, SystemConfigOAuthDto } from '@api';
|
||||
import type { SystemConfigDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import ConfirmDisableLogin from '../confirm-disable-login.svelte';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
import type { ResetOptions } from '$lib/utils/dipatch';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { SettingsEventType } from '../admin-settings';
|
||||
|
||||
export let oauthConfig: SystemConfigOAuthDto;
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
export let config: SystemConfigDto; // this is the config that is being edited
|
||||
export let disabled = false;
|
||||
|
||||
let savedConfig: SystemConfigOAuthDto;
|
||||
let defaultConfig: SystemConfigOAuthDto;
|
||||
|
||||
const handleReset = (detail: ResetOptions) => {
|
||||
if (detail.default) {
|
||||
resetToDefault();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
const dispatch = createEventDispatcher<SettingsEventType>();
|
||||
|
||||
const handleToggleOverride = () => {
|
||||
// click runs before bind
|
||||
const previouslyEnabled = oauthConfig.mobileOverrideEnabled;
|
||||
if (!previouslyEnabled && !oauthConfig.mobileRedirectUri) {
|
||||
oauthConfig.mobileRedirectUri = window.location.origin + '/api/oauth/mobile-redirect';
|
||||
const previouslyEnabled = config.oauth.mobileOverrideEnabled;
|
||||
if (!previouslyEnabled && !config.oauth.mobileRedirectUri) {
|
||||
config.oauth.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.getConfigDefaults().then((res) => res.data.oauth),
|
||||
]);
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
oauthConfig = { ...resetConfig.oauth };
|
||||
savedConfig = { ...resetConfig.oauth };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset OAuth settings to the last saved settings',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
let isConfirmOpen = false;
|
||||
let handleConfirm: (value: boolean) => void;
|
||||
|
||||
@ -67,47 +37,20 @@
|
||||
});
|
||||
};
|
||||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: current } = await api.systemConfigApi.getConfig();
|
||||
|
||||
if (!current.passwordLogin.enabled && current.oauth.enabled && !oauthConfig.enabled) {
|
||||
const confirmed = await openConfirmModal();
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
const handleSave = async () => {
|
||||
if (!savedConfig.passwordLogin.enabled && savedConfig.oauth.enabled && !config.oauth.enabled) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: defaultConfig } = await api.systemConfigApi.getConfigDefaults();
|
||||
if (!config.oauth.mobileOverrideEnabled) {
|
||||
config.oauth.mobileRedirectUri = '';
|
||||
}
|
||||
|
||||
oauthConfig = { ...defaultConfig.oauth };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset OAuth settings to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
dispatch('save', { oauth: config.oauth });
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if isConfirmOpen}
|
||||
@ -115,115 +58,113 @@
|
||||
{/if}
|
||||
|
||||
<div class="mt-2">
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<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">
|
||||
For more details about this feature, refer to the <a
|
||||
href="https://immich.app/docs/administration/oauth#mobile-redirect-uri"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer">docs</a
|
||||
>.
|
||||
</p>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<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">
|
||||
For more details about this feature, refer to the <a
|
||||
href="https://immich.app/docs/administration/oauth#mobile-redirect-uri"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer">docs</a
|
||||
>.
|
||||
</p>
|
||||
|
||||
<SettingSwitch {disabled} title="ENABLE" bind:checked={oauthConfig.enabled} />
|
||||
<hr />
|
||||
<SettingSwitch {disabled} title="ENABLE" bind:checked={config.oauth.enabled} />
|
||||
<hr />
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="ISSUER URL"
|
||||
bind:value={config.oauth.issuerUrl}
|
||||
required={true}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
isEdited={!(config.oauth.issuerUrl == savedConfig.oauth.issuerUrl)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="CLIENT ID"
|
||||
bind:value={config.oauth.clientId}
|
||||
required={true}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
isEdited={!(config.oauth.clientId == savedConfig.oauth.clientId)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="CLIENT SECRET"
|
||||
bind:value={config.oauth.clientSecret}
|
||||
required={true}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
isEdited={!(config.oauth.clientSecret == savedConfig.oauth.clientSecret)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="SCOPE"
|
||||
bind:value={config.oauth.scope}
|
||||
required={true}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
isEdited={!(config.oauth.scope == savedConfig.oauth.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={config.oauth.storageLabelClaim}
|
||||
required={true}
|
||||
disabled={disabled || !config.oauth.storageLabelClaim}
|
||||
isEdited={!(config.oauth.storageLabelClaim == savedConfig.oauth.storageLabelClaim)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="BUTTON TEXT"
|
||||
bind:value={config.oauth.buttonText}
|
||||
required={false}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
isEdited={!(config.oauth.buttonText == savedConfig.oauth.buttonText)}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="AUTO REGISTER"
|
||||
subtitle="Automatically register new users after signing in with OAuth"
|
||||
bind:checked={config.oauth.autoRegister}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="AUTO LAUNCH"
|
||||
subtitle="Start the OAuth login flow automatically upon navigating to the login page"
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
bind:checked={config.oauth.autoLaunch}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="MOBILE REDIRECT URI OVERRIDE"
|
||||
subtitle="Enable when `app.immich:/` is an invalid redirect URI."
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
on:click={() => handleToggleOverride()}
|
||||
bind:checked={config.oauth.mobileOverrideEnabled}
|
||||
/>
|
||||
|
||||
{#if config.oauth.mobileOverrideEnabled}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="ISSUER URL"
|
||||
bind:value={oauthConfig.issuerUrl}
|
||||
label="MOBILE REDIRECT URI"
|
||||
bind:value={config.oauth.mobileRedirectUri}
|
||||
required={true}
|
||||
disabled={disabled || !oauthConfig.enabled}
|
||||
isEdited={!(oauthConfig.issuerUrl == savedConfig.issuerUrl)}
|
||||
disabled={disabled || !config.oauth.enabled}
|
||||
isEdited={!(config.oauth.mobileRedirectUri == savedConfig.oauth.mobileRedirectUri)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="CLIENT ID"
|
||||
bind:value={oauthConfig.clientId}
|
||||
required={true}
|
||||
disabled={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={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={({ detail }) => handleReset(detail)}
|
||||
on:save={saveSetting}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
{disabled}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['oauth'] })}
|
||||
on:save={() => handleSave()}
|
||||
showResetToDefault={!isEqual(savedConfig.oauth, defaultConfig.oauth)}
|
||||
{disabled}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,37 +1,19 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, SystemConfigPasswordLoginDto } from '@api';
|
||||
import type { SystemConfigDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import ConfirmDisableLogin from '../confirm-disable-login.svelte';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
import type { ResetOptions } from '$lib/utils/dipatch';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { SettingsEventType } from '../admin-settings';
|
||||
|
||||
export let passwordLoginConfig: SystemConfigPasswordLoginDto; // this is the config that is being edited
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
export let config: SystemConfigDto; // this is the config that is being edited
|
||||
export let disabled = false;
|
||||
|
||||
let savedConfig: SystemConfigPasswordLoginDto;
|
||||
let defaultConfig: SystemConfigPasswordLoginDto;
|
||||
|
||||
const handleReset = (detail: ResetOptions) => {
|
||||
if (detail.default) {
|
||||
resetToDefault();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.passwordLogin),
|
||||
api.systemConfigApi.getConfigDefaults().then((res) => res.data.passwordLogin),
|
||||
]);
|
||||
}
|
||||
const dispatch = createEventDispatcher<SettingsEventType>();
|
||||
|
||||
let isConfirmOpen = false;
|
||||
let handleConfirm: (value: boolean) => void;
|
||||
@ -46,55 +28,15 @@
|
||||
});
|
||||
};
|
||||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: current } = await api.systemConfigApi.getConfig();
|
||||
|
||||
if (!current.oauth.enabled && current.passwordLogin.enabled && !passwordLoginConfig.enabled) {
|
||||
const confirmed = await openConfirmModal();
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
async function handleSave() {
|
||||
if (!savedConfig.oauth.enabled && savedConfig.passwordLogin.enabled && !config.passwordLogin.enabled) {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
passwordLoginConfig = { ...resetConfig.passwordLogin };
|
||||
savedConfig = { ...resetConfig.passwordLogin };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset settings to the recent saved settings',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
|
||||
|
||||
passwordLoginConfig = { ...configs.passwordLogin };
|
||||
defaultConfig = { ...configs.passwordLogin };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset password settings to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
dispatch('save', { passwordLogin: config.passwordLogin });
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -103,27 +45,23 @@
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ml-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
{disabled}
|
||||
subtitle="Login with email and password"
|
||||
bind:checked={passwordLoginConfig.enabled}
|
||||
/>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
{disabled}
|
||||
subtitle="Login with email and password"
|
||||
bind:checked={config.passwordLogin.enabled}
|
||||
/>
|
||||
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => handleReset(detail)}
|
||||
on:save={saveSetting}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['passwordLogin'] })}
|
||||
on:save={() => handleSave()}
|
||||
showResetToDefault={!isEqual(savedConfig.passwordLogin, defaultConfig.passwordLogin)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,116 +1,49 @@
|
||||
<script lang="ts">
|
||||
import SettingButtonsRow from '$lib/components/admin-page/settings/setting-buttons-row.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import type { ResetOptions } from '$lib/utils/dipatch';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, SystemConfigServerDto } from '@api';
|
||||
import type { SystemConfigDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { SettingsEventType } from '../admin-settings';
|
||||
|
||||
export let serverConfig: SystemConfigServerDto; // this is the config that is being edited
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
export let config: SystemConfigDto; // this is the config that is being edited
|
||||
export let disabled = false;
|
||||
|
||||
let savedConfig: SystemConfigServerDto;
|
||||
let defaultConfig: SystemConfigServerDto;
|
||||
|
||||
const handleReset = (detail: ResetOptions) => {
|
||||
if (detail.default) {
|
||||
resetToDefault();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.server),
|
||||
api.systemConfigApi.getConfigDefaults().then((res) => res.data.server),
|
||||
]);
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
serverConfig = { ...resetConfig.server };
|
||||
savedConfig = { ...resetConfig.server };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset server settings to the recent saved settings',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
|
||||
|
||||
serverConfig = { ...configs.server };
|
||||
defaultConfig = { ...configs.server };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset server settings to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: configs } = await api.systemConfigApi.getConfig();
|
||||
|
||||
const result = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...configs,
|
||||
server: serverConfig,
|
||||
},
|
||||
});
|
||||
|
||||
serverConfig = { ...result.data.server };
|
||||
savedConfig = { ...result.data.server };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Server settings saved',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
} catch (e) {
|
||||
handleError(e, 'Unable to save settings');
|
||||
}
|
||||
}
|
||||
const dispatch = createEventDispatcher<SettingsEventType>();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="mt-2">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="EXTERNAL DOMAIN"
|
||||
desc="Domain for public shared links, including http(s)://"
|
||||
bind:value={serverConfig.externalDomain}
|
||||
isEdited={serverConfig.externalDomain !== savedConfig.externalDomain}
|
||||
/>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="mt-4 ml-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="EXTERNAL DOMAIN"
|
||||
desc="Domain for public shared links, including http(s)://"
|
||||
bind:value={config.server.externalDomain}
|
||||
isEdited={config.server.externalDomain !== savedConfig.server.externalDomain}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="WELCOME MESSAGE"
|
||||
desc="A message that is displayed on the login page."
|
||||
bind:value={serverConfig.loginPageMessage}
|
||||
isEdited={serverConfig.loginPageMessage !== savedConfig.loginPageMessage}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="WELCOME MESSAGE"
|
||||
desc="A message that is displayed on the login page."
|
||||
bind:value={config.server.loginPageMessage}
|
||||
isEdited={config.server.loginPageMessage !== savedConfig.server.loginPageMessage}
|
||||
/>
|
||||
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => handleReset(detail)}
|
||||
on:save={saveSetting}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['server'] })}
|
||||
on:save={() => dispatch('save', { server: config.server })}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { api, SystemConfigStorageTemplateDto, SystemConfigTemplateStorageOptionDto } from '@api';
|
||||
import { api, SystemConfigDto, SystemConfigTemplateStorageOptionDto } from '@api';
|
||||
import * as luxon from 'luxon';
|
||||
import handlebar from 'handlebars';
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
@ -8,51 +8,27 @@
|
||||
import SupportedVariablesPanel from './supported-variables-panel.svelte';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import type { ResetOptions } from '$lib/utils/dipatch';
|
||||
import SettingSwitch from '$lib/components/admin-page/settings/setting-switch.svelte';
|
||||
import Button from '$lib/components/elements/buttons/button.svelte';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import { mdiArrowLeft, mdiCheck } from '@mdi/js';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
import type { SettingsEventType } from '../admin-settings';
|
||||
|
||||
export let storageConfig: SystemConfigStorageTemplateDto;
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
export let config: SystemConfigDto; // this is the config that is being edited
|
||||
export let disabled = false;
|
||||
export let minified = false;
|
||||
|
||||
let savedConfig: SystemConfigStorageTemplateDto;
|
||||
let defaultConfig: SystemConfigStorageTemplateDto;
|
||||
const dispatch = createEventDispatcher<SettingsEventType>();
|
||||
let templateOptions: SystemConfigTemplateStorageOptionDto;
|
||||
let selectedPreset = '';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
save: void;
|
||||
previous: void;
|
||||
}>();
|
||||
|
||||
const handleReset = (detail: ResetOptions) => {
|
||||
if (detail.default) {
|
||||
resetToDefault();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
const getTemplateOptions = async () => {
|
||||
templateOptions = await api.systemConfigApi.getStorageTemplateOptions().then((res) => res.data);
|
||||
selectedPreset = savedConfig.storageTemplate.template;
|
||||
};
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig, templateOptions] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.storageTemplate),
|
||||
api.systemConfigApi.getConfigDefaults().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;
|
||||
@ -60,7 +36,7 @@
|
||||
|
||||
$: parsedTemplate = () => {
|
||||
try {
|
||||
return renderTemplate(storageConfig.template);
|
||||
return renderTemplate(config.storageTemplate.template);
|
||||
} catch (error) {
|
||||
return 'error';
|
||||
}
|
||||
@ -99,78 +75,20 @@
|
||||
return template(substitutions);
|
||||
};
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
storageConfig.template = resetConfig.storageTemplate.template;
|
||||
savedConfig.template = resetConfig.storageTemplate.template;
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset storage template settings to the recent saved settings',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: currentConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
const result = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...currentConfig,
|
||||
storageTemplate: storageConfig,
|
||||
},
|
||||
});
|
||||
|
||||
storageConfig.template = result.data.storageTemplate.template;
|
||||
savedConfig.template = result.data.storageTemplate.template;
|
||||
|
||||
storageConfig.enabled = result.data.storageTemplate.enabled;
|
||||
savedConfig.enabled = result.data.storageTemplate.enabled;
|
||||
|
||||
storageConfig.hashVerificationEnabled = result.data.storageTemplate.hashVerificationEnabled;
|
||||
savedConfig.hashVerificationEnabled = result.data.storageTemplate.hashVerificationEnabled;
|
||||
|
||||
notificationController.show({
|
||||
message: 'Storage template saved',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
|
||||
dispatch('save');
|
||||
} catch (e) {
|
||||
console.error('Error [storage-template-settings] [saveSetting]', e);
|
||||
notificationController.show({
|
||||
message: 'Unable to save settings',
|
||||
type: NotificationType.Error,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: defaultConfig } = await api.systemConfigApi.getConfigDefaults();
|
||||
|
||||
storageConfig.template = defaultConfig.storageTemplate.template;
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset storage template to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
const handlePresetSelection = () => {
|
||||
storageConfig.template = selectedPreset;
|
||||
config.storageTemplate.template = selectedPreset;
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="dark:text-immich-dark-fg">
|
||||
{#await getConfigs() then}
|
||||
<div id="directory-path-builder" class="flex flex-col gap-4 m-4">
|
||||
{#await getTemplateOptions() then}
|
||||
<div id="directory-path-builder" class="flex flex-col gap-4 {minified ? '' : 'ml-4 mt-4'}">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
{disabled}
|
||||
subtitle="Enable storage template engine"
|
||||
bind:checked={storageConfig.enabled}
|
||||
isEdited={!(storageConfig.enabled === savedConfig.enabled)}
|
||||
bind:checked={config.storageTemplate.enabled}
|
||||
isEdited={!(config.storageTemplate.enabled === savedConfig.storageTemplate.enabled)}
|
||||
/>
|
||||
|
||||
{#if !minified}
|
||||
@ -178,12 +96,14 @@
|
||||
title="HASH VERIFICATION ENABLED"
|
||||
{disabled}
|
||||
subtitle="Enables hash verification, don't disable this unless you're certain of the implications"
|
||||
bind:checked={storageConfig.hashVerificationEnabled}
|
||||
isEdited={!(storageConfig.hashVerificationEnabled === savedConfig.hashVerificationEnabled)}
|
||||
bind:checked={config.storageTemplate.hashVerificationEnabled}
|
||||
isEdited={!(
|
||||
config.storageTemplate.hashVerificationEnabled === savedConfig.storageTemplate.hashVerificationEnabled
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if storageConfig.enabled}
|
||||
{#if config.storageTemplate.enabled}
|
||||
<hr />
|
||||
|
||||
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">Variables</h3>
|
||||
@ -232,7 +152,7 @@
|
||||
<label class="text-sm" for="preset-select">PRESET</label>
|
||||
<select
|
||||
class="immich-form-input p-2 mt-2 text-sm rounded-lg bg-slate-200 hover:cursor-pointer dark:bg-gray-600"
|
||||
disabled={disabled || !storageConfig.enabled}
|
||||
disabled={disabled || !config.storageTemplate.enabled}
|
||||
name="presets"
|
||||
id="preset-select"
|
||||
bind:value={selectedPreset}
|
||||
@ -246,11 +166,11 @@
|
||||
<div class="flex gap-2 align-bottom">
|
||||
<SettingInputField
|
||||
label="TEMPLATE"
|
||||
disabled={disabled || !storageConfig.enabled}
|
||||
disabled={disabled || !config.storageTemplate.enabled}
|
||||
required
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
bind:value={storageConfig.template}
|
||||
isEdited={!(storageConfig.template === savedConfig.template)}
|
||||
bind:value={config.storageTemplate.template}
|
||||
isEdited={!(config.storageTemplate.template === savedConfig.storageTemplate.template)}
|
||||
/>
|
||||
|
||||
<div class="flex-0">
|
||||
@ -286,26 +206,11 @@
|
||||
{/if}
|
||||
|
||||
{#if minified}
|
||||
<div class="flex pt-4">
|
||||
<div class="w-full flex place-content-start">
|
||||
<Button class="flex gap-2 place-content-center" on:click={() => dispatch('previous')}>
|
||||
<Icon path={mdiArrowLeft} size="18" />
|
||||
<p>Theme</p>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex w-full place-content-end">
|
||||
<Button on:click={saveSetting}>
|
||||
<span class="flex place-content-center place-items-center gap-2">
|
||||
Done
|
||||
<Icon path={mdiCheck} size="18" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<slot />
|
||||
{:else}
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => handleReset(detail)}
|
||||
on:save={saveSetting}
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['storageTemplate'] })}
|
||||
on:save={() => dispatch('save', { storageTemplate: config.storageTemplate })}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig) && !minified}
|
||||
{disabled}
|
||||
/>
|
||||
|
@ -1,106 +1,40 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, SystemConfigThemeDto } from '@api';
|
||||
import type { SystemConfigDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingTextarea from '../setting-textarea.svelte';
|
||||
import type { ResetOptions } from '$lib/utils/dipatch';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { SettingsEventType } from '../admin-settings';
|
||||
|
||||
export let themeConfig: SystemConfigThemeDto; // this is the config that is being edited
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
export let config: SystemConfigDto; // this is the config that is being edited
|
||||
export let disabled = false;
|
||||
|
||||
let savedConfig: SystemConfigThemeDto;
|
||||
let defaultConfig: SystemConfigThemeDto;
|
||||
|
||||
const handleReset = (detail: ResetOptions) => {
|
||||
if (detail.default) {
|
||||
resetToDefault();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.theme),
|
||||
api.systemConfigApi.getConfigDefaults().then((res) => res.data.theme),
|
||||
]);
|
||||
}
|
||||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: current } = await api.systemConfigApi.getConfig();
|
||||
|
||||
const { data: updated } = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...current,
|
||||
theme: themeConfig,
|
||||
},
|
||||
});
|
||||
|
||||
themeConfig = { ...updated.theme };
|
||||
savedConfig = { ...updated.theme };
|
||||
|
||||
notificationController.show({ message: 'Theme saved', type: NotificationType.Info });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save settings');
|
||||
}
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
themeConfig = { ...resetConfig.theme };
|
||||
savedConfig = { ...resetConfig.theme };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset theme to the recent saved theme',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
|
||||
|
||||
themeConfig = { ...configs.theme };
|
||||
defaultConfig = { ...configs.theme };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset theme to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
const dispatch = createEventDispatcher<SettingsEventType>();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<div class="ml-4">
|
||||
<SettingTextarea
|
||||
{disabled}
|
||||
label="Custom CSS"
|
||||
desc="Cascading Style Sheets allow the design of Immich to be customized."
|
||||
bind:value={themeConfig.customCss}
|
||||
required={true}
|
||||
isEdited={themeConfig.customCss !== savedConfig.customCss}
|
||||
/>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingTextarea
|
||||
{disabled}
|
||||
label="Custom CSS"
|
||||
desc="Cascading Style Sheets allow the design of Immich to be customized."
|
||||
bind:value={config.theme.customCss}
|
||||
required={true}
|
||||
isEdited={config.theme.customCss !== savedConfig.theme.customCss}
|
||||
/>
|
||||
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => handleReset(detail)}
|
||||
on:save={saveSetting}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['theme'] })}
|
||||
on:save={() => dispatch('save', { theme: config.theme })}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,154 +1,84 @@
|
||||
<script lang="ts">
|
||||
import SettingSelect from '$lib/components/admin-page/settings/setting-select.svelte';
|
||||
import { api, Colorspace, SystemConfigThumbnailDto } from '@api';
|
||||
import { Colorspace, SystemConfigDto } from '@api';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import SettingButtonsRow from '$lib/components/admin-page/settings/setting-buttons-row.svelte';
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
import type { ResetOptions } from '$lib/utils/dipatch';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { SettingsEventType } from '../admin-settings';
|
||||
|
||||
export let thumbnailConfig: SystemConfigThumbnailDto; // this is the config that is being edited
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
export let config: SystemConfigDto; // this is the config that is being edited
|
||||
export let disabled = false;
|
||||
|
||||
let savedConfig: SystemConfigThumbnailDto;
|
||||
let defaultConfig: SystemConfigThumbnailDto;
|
||||
|
||||
const handleReset = (detail: ResetOptions) => {
|
||||
if (detail.default) {
|
||||
resetToDefault();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.thumbnail),
|
||||
api.systemConfigApi.getConfigDefaults().then((res) => res.data.thumbnail),
|
||||
]);
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
thumbnailConfig = { ...resetConfig.thumbnail };
|
||||
savedConfig = { ...resetConfig.thumbnail };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset thumbnail settings to the recent saved settings',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
|
||||
|
||||
thumbnailConfig = { ...configs.thumbnail };
|
||||
defaultConfig = { ...configs.thumbnail };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset thumbnail settings to default',
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
const dispatch = createEventDispatcher<SettingsEventType>();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSelect
|
||||
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."
|
||||
number
|
||||
bind:value={thumbnailConfig.webpSize}
|
||||
options={[
|
||||
{ value: 1080, text: '1080p' },
|
||||
{ value: 720, text: '720p' },
|
||||
{ value: 480, text: '480p' },
|
||||
{ value: 250, text: '250p' },
|
||||
{ value: 200, text: '200p' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={thumbnailConfig.webpSize !== savedConfig.webpSize}
|
||||
{disabled}
|
||||
/>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSelect
|
||||
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."
|
||||
number
|
||||
bind:value={config.thumbnail.webpSize}
|
||||
options={[
|
||||
{ value: 1080, text: '1080p' },
|
||||
{ value: 720, text: '720p' },
|
||||
{ value: 480, text: '480p' },
|
||||
{ value: 250, text: '250p' },
|
||||
{ value: 200, text: '200p' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={config.thumbnail.webpSize !== savedConfig.thumbnail.webpSize}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
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."
|
||||
number
|
||||
bind:value={thumbnailConfig.jpegSize}
|
||||
options={[
|
||||
{ value: 2160, text: '4K' },
|
||||
{ value: 1440, text: '1440p' },
|
||||
{ value: 1080, text: '1080p' },
|
||||
{ value: 720, text: '720p' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={thumbnailConfig.jpegSize !== savedConfig.jpegSize}
|
||||
{disabled}
|
||||
/>
|
||||
<SettingSelect
|
||||
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."
|
||||
number
|
||||
bind:value={config.thumbnail.jpegSize}
|
||||
options={[
|
||||
{ value: 2160, text: '4K' },
|
||||
{ value: 1440, text: '1440p' },
|
||||
{ value: 1080, text: '1080p' },
|
||||
{ value: 720, text: '720p' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={config.thumbnail.jpegSize !== savedConfig.thumbnail.jpegSize}
|
||||
{disabled}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="QUALITY"
|
||||
desc="Thumbnail quality from 1-100. Higher is better for quality but produces larger files."
|
||||
bind:value={thumbnailConfig.quality}
|
||||
isEdited={thumbnailConfig.quality !== savedConfig.quality}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="QUALITY"
|
||||
desc="Thumbnail quality from 1-100. Higher is better for quality but produces larger files."
|
||||
bind:value={config.thumbnail.quality}
|
||||
isEdited={config.thumbnail.quality !== savedConfig.thumbnail.quality}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="PREFER WIDE GAMUT"
|
||||
subtitle="Use Display P3 for thumbnails. This better preserves the vibrance of images with wide colorspaces, but images may appear differently on old devices with an old browser version. sRGB images are kept as sRGB to avoid color shifts."
|
||||
checked={thumbnailConfig.colorspace === Colorspace.P3}
|
||||
on:toggle={(e) => (thumbnailConfig.colorspace = e.detail ? Colorspace.P3 : Colorspace.Srgb)}
|
||||
isEdited={thumbnailConfig.colorspace !== savedConfig.colorspace}
|
||||
/>
|
||||
</div>
|
||||
<SettingSwitch
|
||||
title="PREFER WIDE GAMUT"
|
||||
subtitle="Use Display P3 for thumbnails. This better preserves the vibrance of images with wide colorspaces, but images may appear differently on old devices with an old browser version. sRGB images are kept as sRGB to avoid color shifts."
|
||||
checked={config.thumbnail.colorspace === Colorspace.P3}
|
||||
on:toggle={(e) => (config.thumbnail.colorspace = e.detail ? Colorspace.P3 : Colorspace.Srgb)}
|
||||
isEdited={config.thumbnail.colorspace !== savedConfig.thumbnail.colorspace}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => handleReset(detail)}
|
||||
on:save={saveSetting}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['thumbnail'] })}
|
||||
on:save={() => dispatch('save', { thumbnail: config.thumbnail })}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,111 +1,51 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, SystemConfigTrashDto } from '@api';
|
||||
import type { SystemConfigDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
import type { ResetOptions } from '$lib/utils/dipatch';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { SettingsEventType } from '../admin-settings';
|
||||
|
||||
export let trashConfig: SystemConfigTrashDto; // this is the config that is being edited
|
||||
export let savedConfig: SystemConfigDto;
|
||||
export let defaultConfig: SystemConfigDto;
|
||||
export let config: SystemConfigDto; // this is the config that is being edited
|
||||
export let disabled = false;
|
||||
|
||||
let savedConfig: SystemConfigTrashDto;
|
||||
let defaultConfig: SystemConfigTrashDto;
|
||||
|
||||
const handleReset = (detail: ResetOptions) => {
|
||||
if (detail.default) {
|
||||
resetToDefault();
|
||||
} else {
|
||||
reset();
|
||||
}
|
||||
};
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.trash),
|
||||
api.systemConfigApi.getConfigDefaults().then((res) => res.data.trash),
|
||||
]);
|
||||
}
|
||||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: current } = await api.systemConfigApi.getConfig();
|
||||
const { data: updated } = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: { ...current, trash: trashConfig },
|
||||
});
|
||||
|
||||
trashConfig = { ...updated.trash };
|
||||
savedConfig = { ...updated.trash };
|
||||
|
||||
notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
|
||||
} catch (error) {
|
||||
handleError(error, 'Unable to save settings');
|
||||
}
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
trashConfig = { ...resetConfig.trash };
|
||||
savedConfig = { ...resetConfig.trash };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset settings to the recent saved settings',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getConfigDefaults();
|
||||
|
||||
trashConfig = { ...configs.trash };
|
||||
defaultConfig = { ...configs.trash };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset trash settings to default',
|
||||
type: NotificationType.Info,
|
||||
});
|
||||
}
|
||||
const dispatch = createEventDispatcher<SettingsEventType>();
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#await getConfigs() then}
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
{disabled}
|
||||
subtitle="Enable Trash features"
|
||||
bind:checked={trashConfig.enabled}
|
||||
/>
|
||||
<div in:fade={{ duration: 500 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSwitch
|
||||
title="ENABLED"
|
||||
{disabled}
|
||||
subtitle="Enable Trash features"
|
||||
bind:checked={config.trash.enabled}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
<hr />
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="Number of days"
|
||||
desc="Number of days to keep the assets in trash before permanently removing them"
|
||||
bind:value={trashConfig.days}
|
||||
required={true}
|
||||
disabled={disabled || !trashConfig.enabled}
|
||||
isEdited={trashConfig.days !== savedConfig.days}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="Number of days"
|
||||
desc="Number of days to keep the assets in trash before permanently removing them"
|
||||
bind:value={config.trash.days}
|
||||
required={true}
|
||||
disabled={disabled || !config.trash.enabled}
|
||||
isEdited={config.trash.days !== savedConfig.trash.days}
|
||||
/>
|
||||
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => handleReset(detail)}
|
||||
on:save={saveSetting}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
<SettingButtonsRow
|
||||
on:reset={({ detail }) => dispatch('reset', { ...detail, configKeys: ['trash'] })}
|
||||
on:save={() => dispatch('save', { trash: config.trash })}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
{disabled}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,17 +5,21 @@
|
||||
import StorageTemplateSettings from '../admin-page/settings/storage-template/storage-template-settings.svelte';
|
||||
import { SystemConfigDto, api } from '@api';
|
||||
import { user } from '$lib/stores/user.store';
|
||||
import AdminSettings from '../admin-page/settings/admin-settings.svelte';
|
||||
import { mdiArrowLeft, mdiCheck } from '@mdi/js';
|
||||
import Button from '../elements/buttons/button.svelte';
|
||||
import Icon from '../elements/icon.svelte';
|
||||
|
||||
const dispatch = createEventDispatcher<{
|
||||
done: void;
|
||||
previous: void;
|
||||
}>();
|
||||
|
||||
let configs: SystemConfigDto | null = null;
|
||||
let config: SystemConfigDto | null = null;
|
||||
|
||||
onMount(async () => {
|
||||
const { data } = await api.systemConfigApi.getConfig();
|
||||
configs = data;
|
||||
config = data;
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -27,13 +31,39 @@
|
||||
variables to customize the template to your liking.
|
||||
</p>
|
||||
|
||||
{#if configs && $user}
|
||||
<StorageTemplateSettings
|
||||
minified
|
||||
disabled={$featureFlags.configFile}
|
||||
storageConfig={configs.storageTemplate}
|
||||
on:save={() => dispatch('done')}
|
||||
on:previous={() => dispatch('previous')}
|
||||
/>
|
||||
{#if config && $user}
|
||||
<AdminSettings bind:config let:defaultConfig let:savedConfig let:handleSave let:handleReset>
|
||||
<StorageTemplateSettings
|
||||
minified
|
||||
disabled={$featureFlags.configFile}
|
||||
{config}
|
||||
{defaultConfig}
|
||||
{savedConfig}
|
||||
on:save={({ detail }) => handleSave(detail)}
|
||||
on:reset={({ detail }) => handleReset(detail)}
|
||||
>
|
||||
<div class="flex pt-4">
|
||||
<div class="w-full flex place-content-start">
|
||||
<Button class="flex gap-2 place-content-center" on:click={() => dispatch('previous')}>
|
||||
<Icon path={mdiArrowLeft} size="18" />
|
||||
<p>Theme</p>
|
||||
</Button>
|
||||
</div>
|
||||
<div class="flex w-full place-content-end">
|
||||
<Button
|
||||
on:click={() => {
|
||||
handleSave({ storageTemplate: config?.storageTemplate });
|
||||
dispatch('done');
|
||||
}}
|
||||
>
|
||||
<span class="flex place-content-center place-items-center gap-2">
|
||||
Done
|
||||
<Icon path={mdiCheck} size="18" />
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</StorageTemplateSettings>
|
||||
</AdminSettings>
|
||||
{/if}
|
||||
</OnboardingCard>
|
||||
|
@ -32,7 +32,7 @@
|
||||
|
||||
<UserPageLayout title={data.meta.title} admin>
|
||||
<div class="flex justify-end" slot="buttons">
|
||||
<a href="{AppRoute.ADMIN_SETTINGS}?open=job-settings">
|
||||
<a href="{AppRoute.ADMIN_SETTINGS}?open=job">
|
||||
<LinkButton>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiCog} size="18" />
|
||||
|
@ -17,26 +17,116 @@
|
||||
import { downloadManager } from '$lib/stores/download';
|
||||
import { featureFlags } from '$lib/stores/server-config.store';
|
||||
import { downloadBlob } from '$lib/utils/asset-utils';
|
||||
import { copyToClipboard } from '@api';
|
||||
import { SystemConfigDto, copyToClipboard } from '@api';
|
||||
import Icon from '$lib/components/elements/icon.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import NewVersionCheckSettings from '$lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte';
|
||||
import LibrarySettings from '$lib/components/admin-page/settings/library-settings/library-settings.svelte';
|
||||
import LoggingSettings from '$lib/components/admin-page/settings/logging-settings/logging-settings.svelte';
|
||||
import { mdiAlert, mdiContentCopy, mdiDownload } from '@mdi/js';
|
||||
import _ from 'lodash';
|
||||
import AdminSettings from '$lib/components/admin-page/settings/admin-settings.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
const configs = data.configs;
|
||||
let config = data.configs;
|
||||
let openSettings = ($page.url.searchParams.get('open')?.split(',') || []) as Array<keyof SystemConfigDto>;
|
||||
|
||||
const downloadConfig = () => {
|
||||
const blob = new Blob([JSON.stringify(configs, null, 2)], { type: 'application/json' });
|
||||
const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' });
|
||||
const downloadKey = 'immich-config.json';
|
||||
downloadManager.add(downloadKey, blob.size);
|
||||
downloadManager.update(downloadKey, blob.size);
|
||||
downloadBlob(blob, downloadKey);
|
||||
setTimeout(() => downloadManager.clear(downloadKey), 5_000);
|
||||
};
|
||||
|
||||
const settings = [
|
||||
{
|
||||
item: JobSettings,
|
||||
title: 'Job Settings',
|
||||
subtitle: 'Manage job concurrency',
|
||||
isOpen: openSettings.includes('job'),
|
||||
},
|
||||
{
|
||||
item: LibrarySettings,
|
||||
title: 'Library',
|
||||
subtitle: 'Manage library settings',
|
||||
isOpen: openSettings.includes('library'),
|
||||
},
|
||||
{
|
||||
item: LoggingSettings,
|
||||
title: 'Logging',
|
||||
subtitle: 'Manage log settings',
|
||||
isOpen: openSettings.includes('logging'),
|
||||
},
|
||||
{
|
||||
item: MachineLearningSettings,
|
||||
title: 'Machine Learning Settings',
|
||||
subtitle: 'Manage machine learning features and settings',
|
||||
isOpen: openSettings.includes('machineLearning'),
|
||||
},
|
||||
{
|
||||
item: MapSettings,
|
||||
title: 'Map & GPS Settings',
|
||||
subtitle: 'Manage map related features and setting',
|
||||
isOpen: openSettings.some((key) => ['map', 'reverseGeocoding'].includes(key)),
|
||||
},
|
||||
{
|
||||
item: OAuthSettings,
|
||||
title: 'OAuth Authentication',
|
||||
subtitle: 'Manage the login with OAuth settings',
|
||||
isOpen: openSettings.includes('oauth'),
|
||||
},
|
||||
{
|
||||
item: PasswordLoginSettings,
|
||||
title: 'Password Authentication',
|
||||
subtitle: 'Manage the login with password settings',
|
||||
isOpen: openSettings.includes('passwordLogin'),
|
||||
},
|
||||
{
|
||||
item: ServerSettings,
|
||||
title: 'Server Settings',
|
||||
subtitle: 'Manage server settings',
|
||||
isOpen: openSettings.includes('server'),
|
||||
},
|
||||
{
|
||||
item: StorageTemplateSettings,
|
||||
title: 'Storage Template',
|
||||
subtitle: 'Manage the folder structure and file name of the upload asset',
|
||||
isOpen: openSettings.includes('storageTemplate'),
|
||||
},
|
||||
{
|
||||
item: ThemeSettings,
|
||||
title: 'Theme Settings',
|
||||
subtitle: 'Manage customization of the Immich web interface',
|
||||
isOpen: openSettings.includes('theme'),
|
||||
},
|
||||
{
|
||||
item: ThumbnailSettings,
|
||||
title: 'Thumbnail Settings',
|
||||
subtitle: 'Manage the resolution of thumbnail sizes',
|
||||
isOpen: openSettings.includes('thumbnail'),
|
||||
},
|
||||
{
|
||||
item: TrashSettings,
|
||||
title: 'Trash Settings',
|
||||
subtitle: 'Manage trash settings',
|
||||
isOpen: openSettings.includes('trash'),
|
||||
},
|
||||
{
|
||||
item: NewVersionCheckSettings,
|
||||
title: 'Version Check',
|
||||
subtitle: 'Enable/disable the new version notification',
|
||||
isOpen: openSettings.includes('newVersionCheck'),
|
||||
},
|
||||
{
|
||||
item: FFmpegSettings,
|
||||
title: 'Video Transcoding Settings',
|
||||
subtitle: 'Manage the resolution and encoding information of the video files',
|
||||
isOpen: openSettings.includes('ffmpeg'),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
{#if $featureFlags.configFile}
|
||||
@ -48,7 +138,7 @@
|
||||
|
||||
<UserPageLayout title={data.meta.title} admin>
|
||||
<div class="flex justify-end gap-2" slot="buttons">
|
||||
<LinkButton on:click={() => copyToClipboard(JSON.stringify(configs, null, 2))}>
|
||||
<LinkButton on:click={() => copyToClipboard(JSON.stringify(config, null, 2))}>
|
||||
<div class="flex place-items-center gap-2 text-sm">
|
||||
<Icon path={mdiContentCopy} size="18" />
|
||||
Copy to Clipboard
|
||||
@ -62,74 +152,23 @@
|
||||
</LinkButton>
|
||||
</div>
|
||||
|
||||
<section id="setting-content" class="flex place-content-center sm:mx-4">
|
||||
<section class="w-full pb-28 sm:w-5/6 md:w-[850px]">
|
||||
<SettingAccordion
|
||||
title="Job Settings"
|
||||
subtitle="Manage job concurrency"
|
||||
isOpen={$page.url.searchParams.get('open') === 'job-settings'}
|
||||
>
|
||||
<JobSettings disabled={$featureFlags.configFile} jobConfig={configs.job} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Library" subtitle="Manage library settings">
|
||||
<LibrarySettings disabled={$featureFlags.configFile} libraryConfig={configs.library} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Logging" subtitle="Manage log settings">
|
||||
<LoggingSettings disabled={$featureFlags.configFile} loggingConfig={configs.logging} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Machine Learning Settings" subtitle="Manage machine learning features and settings">
|
||||
<MachineLearningSettings disabled={$featureFlags.configFile} machineLearningConfig={configs.machineLearning} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Map & GPS Settings" subtitle="Manage map related features and setting">
|
||||
<MapSettings disabled={$featureFlags.configFile} config={configs} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="OAuth Authentication" subtitle="Manage the login with OAuth settings">
|
||||
<OAuthSettings disabled={$featureFlags.configFile} oauthConfig={configs.oauth} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Password Authentication" subtitle="Manage login with password settings">
|
||||
<PasswordLoginSettings disabled={$featureFlags.configFile} passwordLoginConfig={configs.passwordLogin} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Server Settings" subtitle="Manage server settings">
|
||||
<ServerSettings disabled={$featureFlags.configFile} serverConfig={configs.server} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
title="Storage Template"
|
||||
subtitle="Manage the folder structure and file name of the upload asset"
|
||||
isOpen={$page.url.searchParams.get('open') === 'storage-template'}
|
||||
>
|
||||
<StorageTemplateSettings disabled={$featureFlags.configFile} storageConfig={configs.storageTemplate} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Theme Settings" subtitle="Manage customization of the Immich web interface">
|
||||
<ThemeSettings disabled={$featureFlags.configFile} themeConfig={configs.theme} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Thumbnail Settings" subtitle="Manage the resolution of thumbnail sizes">
|
||||
<ThumbnailSettings disabled={$featureFlags.configFile} thumbnailConfig={configs.thumbnail} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Trash Settings" subtitle="Manage trash settings">
|
||||
<TrashSettings disabled={$featureFlags.configFile} trashConfig={configs.trash} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Version Check" subtitle="Enable/disable the new version notification">
|
||||
<NewVersionCheckSettings disabled={$featureFlags.configFile} newVersionCheckConfig={configs.newVersionCheck} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
title="Video Transcoding Settings"
|
||||
subtitle="Manage the resolution and encoding information of the video files"
|
||||
>
|
||||
<FFmpegSettings disabled={$featureFlags.configFile} ffmpegConfig={configs.ffmpeg} />
|
||||
</SettingAccordion>
|
||||
<AdminSettings bind:config let:handleReset let:handleSave let:savedConfig let:defaultConfig>
|
||||
<section id="setting-content" class="flex place-content-center sm:mx-4">
|
||||
<section class="w-full pb-28 sm:w-5/6 md:w-[850px]">
|
||||
{#each settings as { item, title, subtitle, isOpen }}
|
||||
<SettingAccordion {title} {subtitle} {isOpen}>
|
||||
<svelte:component
|
||||
this={item}
|
||||
on:save={({ detail }) => handleSave(detail)}
|
||||
on:reset={({ detail }) => handleReset(detail)}
|
||||
disabled={$featureFlags.configFile}
|
||||
{defaultConfig}
|
||||
{config}
|
||||
{savedConfig}
|
||||
/>
|
||||
</SettingAccordion>
|
||||
{/each}
|
||||
</section>
|
||||
</section>
|
||||
</section>
|
||||
</AdminSettings>
|
||||
</UserPageLayout>
|
||||
|
Loading…
Reference in New Issue
Block a user