refactor(web): system-settings
This commit is contained in:
parent
5fa9704a65
commit
0e4b00d07a
7 changed files with 548 additions and 613 deletions
|
@ -6,6 +6,7 @@
|
|||
import {
|
||||
api,
|
||||
AudioCodec,
|
||||
SystemConfigDto,
|
||||
SystemConfigFFmpegDto,
|
||||
ToneMapping,
|
||||
TranscodeHWAccel,
|
||||
|
@ -19,31 +20,23 @@
|
|||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
export let config: SystemConfigDto;
|
||||
export let ffmpegConfig: SystemConfigFFmpegDto; // this is the config that is being edited
|
||||
|
||||
let savedConfig: SystemConfigFFmpegDto;
|
||||
let defaultConfig: SystemConfigFFmpegDto;
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.ffmpeg),
|
||||
api.systemConfigApi.getDefaults().then((res) => res.data.ffmpeg),
|
||||
]);
|
||||
}
|
||||
export let ffmpegDefault: SystemConfigFFmpegDto;
|
||||
export let savedConfig: SystemConfigFFmpegDto;
|
||||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: configs } = await api.systemConfigApi.getConfig();
|
||||
|
||||
const result = await api.systemConfigApi.updateConfig({
|
||||
const { data } = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...configs,
|
||||
...config,
|
||||
ffmpeg: ffmpegConfig,
|
||||
},
|
||||
});
|
||||
|
||||
ffmpegConfig = { ...result.data.ffmpeg };
|
||||
savedConfig = { ...result.data.ffmpeg };
|
||||
ffmpegConfig = { ...data.ffmpeg };
|
||||
savedConfig = { ...data.ffmpeg };
|
||||
config = { ...data };
|
||||
|
||||
notificationController.show({
|
||||
message: 'FFmpeg settings saved',
|
||||
|
@ -59,10 +52,7 @@
|
|||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
ffmpegConfig = { ...resetConfig.ffmpeg };
|
||||
savedConfig = { ...resetConfig.ffmpeg };
|
||||
ffmpegConfig = { ...savedConfig };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset FFmpeg settings to the recent saved settings',
|
||||
|
@ -71,10 +61,7 @@
|
|||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getDefaults();
|
||||
|
||||
ffmpegConfig = { ...configs.ffmpeg };
|
||||
defaultConfig = { ...configs.ffmpeg };
|
||||
ffmpegConfig = ffmpegDefault;
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset FFmpeg settings to default',
|
||||
|
@ -84,185 +71,183 @@
|
|||
</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">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="CONSTANT RATE FACTOR (-crf)"
|
||||
desc="Video quality level. Typical values are 23 for H.264, 28 for HEVC, and 31 for VP9. Lower is better, but takes longer to encode and produces larger files."
|
||||
bind:value={ffmpegConfig.crf}
|
||||
required={true}
|
||||
isEdited={!(ffmpegConfig.crf == savedConfig.crf)}
|
||||
/>
|
||||
<div in:fade={{ duration: 300 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="CONSTANT RATE FACTOR (-crf)"
|
||||
desc="Video quality level. Typical values are 23 for H.264, 28 for HEVC, and 31 for VP9. Lower is better, but takes longer to encode and produces larger files."
|
||||
bind:value={ffmpegConfig.crf}
|
||||
required={true}
|
||||
isEdited={!(ffmpegConfig.crf == savedConfig.crf)}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="PRESET (-preset)"
|
||||
desc="Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above `faster`."
|
||||
bind:value={ffmpegConfig.preset}
|
||||
name="preset"
|
||||
options={[
|
||||
{ value: 'ultrafast', text: 'ultrafast' },
|
||||
{ value: 'superfast', text: 'superfast' },
|
||||
{ value: 'veryfast', text: 'veryfast' },
|
||||
{ value: 'faster', text: 'faster' },
|
||||
{ value: 'fast', text: 'fast' },
|
||||
{ value: 'medium', text: 'medium' },
|
||||
{ value: 'slow', text: 'slow' },
|
||||
{ value: 'slower', text: 'slower' },
|
||||
{ value: 'veryslow', text: 'veryslow' },
|
||||
]}
|
||||
isEdited={!(ffmpegConfig.preset == savedConfig.preset)}
|
||||
/>
|
||||
<SettingSelect
|
||||
label="PRESET (-preset)"
|
||||
desc="Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above `faster`."
|
||||
bind:value={ffmpegConfig.preset}
|
||||
name="preset"
|
||||
options={[
|
||||
{ value: 'ultrafast', text: 'ultrafast' },
|
||||
{ value: 'superfast', text: 'superfast' },
|
||||
{ value: 'veryfast', text: 'veryfast' },
|
||||
{ value: 'faster', text: 'faster' },
|
||||
{ value: 'fast', text: 'fast' },
|
||||
{ value: 'medium', text: 'medium' },
|
||||
{ value: 'slow', text: 'slow' },
|
||||
{ value: 'slower', text: 'slower' },
|
||||
{ value: 'veryslow', text: 'veryslow' },
|
||||
]}
|
||||
isEdited={!(ffmpegConfig.preset == savedConfig.preset)}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="AUDIO CODEC"
|
||||
desc="Opus is the highest quality option, but has lower compatibility with old devices or software."
|
||||
bind:value={ffmpegConfig.targetAudioCodec}
|
||||
options={[
|
||||
{ value: AudioCodec.Aac, text: 'aac' },
|
||||
{ value: AudioCodec.Mp3, text: 'mp3' },
|
||||
{ value: AudioCodec.Opus, text: 'opus' },
|
||||
]}
|
||||
name="acodec"
|
||||
isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)}
|
||||
/>
|
||||
<SettingSelect
|
||||
label="AUDIO CODEC"
|
||||
desc="Opus is the highest quality option, but has lower compatibility with old devices or software."
|
||||
bind:value={ffmpegConfig.targetAudioCodec}
|
||||
options={[
|
||||
{ value: AudioCodec.Aac, text: 'aac' },
|
||||
{ value: AudioCodec.Mp3, text: 'mp3' },
|
||||
{ value: AudioCodec.Opus, text: 'opus' },
|
||||
]}
|
||||
name="acodec"
|
||||
isEdited={!(ffmpegConfig.targetAudioCodec == savedConfig.targetAudioCodec)}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="VIDEO CODEC"
|
||||
desc="VP9 has high efficiency and web compatibility, but takes longer to transcode. HEVC performs similarly, but has lower web compatibility. H.264 is widely compatible and quick to transcode, but produces much larger files."
|
||||
bind:value={ffmpegConfig.targetVideoCodec}
|
||||
options={[
|
||||
{ value: VideoCodec.H264, text: 'h264' },
|
||||
{ value: VideoCodec.Hevc, text: 'hevc' },
|
||||
{ value: VideoCodec.Vp9, text: 'vp9' },
|
||||
]}
|
||||
name="vcodec"
|
||||
isEdited={!(ffmpegConfig.targetVideoCodec == savedConfig.targetVideoCodec)}
|
||||
/>
|
||||
<SettingSelect
|
||||
label="VIDEO CODEC"
|
||||
desc="VP9 has high efficiency and web compatibility, but takes longer to transcode. HEVC performs similarly, but has lower web compatibility. H.264 is widely compatible and quick to transcode, but produces much larger files."
|
||||
bind:value={ffmpegConfig.targetVideoCodec}
|
||||
options={[
|
||||
{ value: 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"
|
||||
desc="Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
||||
bind:value={ffmpegConfig.targetResolution}
|
||||
options={[
|
||||
{ value: '2160', text: '4k' },
|
||||
{ value: '1440', text: '1440p' },
|
||||
{ value: '1080', text: '1080p' },
|
||||
{ value: '720', text: '720p' },
|
||||
{ value: '480', text: '480p' },
|
||||
{ value: 'original', text: 'original' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={!(ffmpegConfig.targetResolution == savedConfig.targetResolution)}
|
||||
/>
|
||||
<SettingSelect
|
||||
label="TARGET RESOLUTION"
|
||||
desc="Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness."
|
||||
bind:value={ffmpegConfig.targetResolution}
|
||||
options={[
|
||||
{ value: '2160', text: '4k' },
|
||||
{ value: '1440', text: '1440p' },
|
||||
{ value: '1080', text: '1080p' },
|
||||
{ value: '720', text: '720p' },
|
||||
{ value: '480', text: '480p' },
|
||||
{ value: 'original', text: 'original' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={!(ffmpegConfig.targetResolution == savedConfig.targetResolution)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="MAX BITRATE"
|
||||
desc="Setting a max bitrate can make file sizes more predictable at a minor cost to quality. At 720p, typical values are 2600k for VP9 or HEVC, or 4500k for H.264. Disabled if set to 0."
|
||||
bind:value={ffmpegConfig.maxBitrate}
|
||||
isEdited={!(ffmpegConfig.maxBitrate == savedConfig.maxBitrate)}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="MAX BITRATE"
|
||||
desc="Setting a max bitrate can make file sizes more predictable at a minor cost to quality. At 720p, typical values are 2600k for VP9 or HEVC, or 4500k for H.264. Disabled if set to 0."
|
||||
bind:value={ffmpegConfig.maxBitrate}
|
||||
isEdited={!(ffmpegConfig.maxBitrate == savedConfig.maxBitrate)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="THREADS"
|
||||
desc="Higher values lead to faster encoding, but leave less room for the server to process other tasks while active. This value should not be more than the number of CPU cores. Maximizes utilization if set to 0."
|
||||
bind:value={ffmpegConfig.threads}
|
||||
isEdited={!(ffmpegConfig.threads == savedConfig.threads)}
|
||||
/>
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.NUMBER}
|
||||
label="THREADS"
|
||||
desc="Higher values lead to faster encoding, but leave less room for the server to process other tasks while active. This value should not be more than the number of CPU cores. Maximizes utilization if set to 0."
|
||||
bind:value={ffmpegConfig.threads}
|
||||
isEdited={!(ffmpegConfig.threads == savedConfig.threads)}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="TRANSCODE POLICY"
|
||||
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="TRANSCODE POLICY"
|
||||
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="HARDWARE ACCELERATION"
|
||||
desc="Experimental. Much faster, but will have lower quality at the same bitrate. This setting is 'best effort': it will fallback to software transcoding on failure. VP9 may or may not work depending on your hardware."
|
||||
bind:value={ffmpegConfig.accel}
|
||||
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.Disabled,
|
||||
text: 'Disabled',
|
||||
},
|
||||
]}
|
||||
isEdited={!(ffmpegConfig.accel == savedConfig.accel)}
|
||||
/>
|
||||
<SettingSelect
|
||||
label="HARDWARE ACCELERATION"
|
||||
desc="Experimental. Much faster, but will have lower quality at the same bitrate. This setting is 'best effort': it will fallback to software transcoding on failure. VP9 may or may not work depending on your hardware."
|
||||
bind:value={ffmpegConfig.accel}
|
||||
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.Disabled,
|
||||
text: 'Disabled',
|
||||
},
|
||||
]}
|
||||
isEdited={!(ffmpegConfig.accel == savedConfig.accel)}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="TONE-MAPPING"
|
||||
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)}
|
||||
/>
|
||||
<SettingSelect
|
||||
label="TONE-MAPPING"
|
||||
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"
|
||||
subtitle="Transcode in two passes to produce better encoded videos. When max bitrate is enabled (required for it to work with H.264 and HEVC), this mode uses a bitrate range based on the max bitrate and ignores CRF. For VP9, CRF can be used if max bitrate is disabled."
|
||||
bind:checked={ffmpegConfig.twoPass}
|
||||
isEdited={!(ffmpegConfig.twoPass === savedConfig.twoPass)}
|
||||
/>
|
||||
</div>
|
||||
<SettingSwitch
|
||||
title="TWO-PASS ENCODING"
|
||||
subtitle="Transcode in two passes to produce better encoded videos. When max bitrate is enabled (required for it to work with H.264 and HEVC), this mode uses a bitrate range based on the max bitrate and ignores CRF. For VP9, CRF can be used if max bitrate is disabled."
|
||||
bind:checked={ffmpegConfig.twoPass}
|
||||
isEdited={!(ffmpegConfig.twoPass === savedConfig.twoPass)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(ffmpegConfig, ffmpegDefault)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -3,41 +3,33 @@
|
|||
notificationController,
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { api, JobName, SystemConfigJobDto } from '@api';
|
||||
import { api, JobName, SystemConfigDto, SystemConfigJobDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { handleError } from '../../../../utils/handle-error';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
|
||||
export let config: SystemConfigDto;
|
||||
export let jobConfig: SystemConfigJobDto; // this is the config that is being edited
|
||||
|
||||
let savedConfig: SystemConfigJobDto;
|
||||
let defaultConfig: SystemConfigJobDto;
|
||||
export let jobDefault: SystemConfigJobDto;
|
||||
export let savedConfig: SystemConfigJobDto;
|
||||
|
||||
const ignoredJobs = [JobName.BackgroundTask, JobName.Search] as JobName[];
|
||||
const jobNames = Object.values(JobName).filter((jobName) => !ignoredJobs.includes(jobName as JobName));
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.job),
|
||||
api.systemConfigApi.getDefaults().then((res) => res.data.job),
|
||||
]);
|
||||
}
|
||||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: configs } = await api.systemConfigApi.getConfig();
|
||||
|
||||
const result = await api.systemConfigApi.updateConfig({
|
||||
const { data } = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...configs,
|
||||
...config,
|
||||
job: jobConfig,
|
||||
},
|
||||
});
|
||||
|
||||
jobConfig = { ...result.data.job };
|
||||
savedConfig = { ...result.data.job };
|
||||
jobConfig = { ...data.job };
|
||||
savedConfig = { ...data.job };
|
||||
config = { ...data };
|
||||
|
||||
notificationController.show({ message: 'Job settings saved', type: NotificationType.Info });
|
||||
} catch (error) {
|
||||
|
@ -46,10 +38,7 @@
|
|||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
jobConfig = { ...resetConfig.job };
|
||||
savedConfig = { ...resetConfig.job };
|
||||
jobConfig = { ...savedConfig };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset Job settings to the recent saved settings',
|
||||
|
@ -58,10 +47,7 @@
|
|||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getDefaults();
|
||||
|
||||
jobConfig = { ...configs.job };
|
||||
defaultConfig = { ...configs.job };
|
||||
jobConfig = jobDefault;
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset Job settings to default',
|
||||
|
@ -71,31 +57,29 @@
|
|||
</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}
|
||||
label="{api.getJobName(jobName)} Concurrency"
|
||||
desc=""
|
||||
bind:value={jobConfig[jobName].concurrency}
|
||||
required={true}
|
||||
isEdited={!(jobConfig[jobName].concurrency == savedConfig[jobName].concurrency)}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
<div in:fade={{ duration: 300 }}>
|
||||
<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}
|
||||
label="{api.getJobName(jobName)} Concurrency"
|
||||
desc=""
|
||||
bind:value={jobConfig[jobName].concurrency}
|
||||
required={true}
|
||||
isEdited={!(jobConfig[jobName].concurrency == savedConfig[jobName].concurrency)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
{/each}
|
||||
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(jobConfig, jobDefault)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, SystemConfigOAuthDto } from '@api';
|
||||
import { api, SystemConfigDto, SystemConfigOAuthDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import ConfirmDisableLogin from '../confirm-disable-login.svelte';
|
||||
|
@ -12,10 +12,10 @@
|
|||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
|
||||
export let config: SystemConfigDto;
|
||||
export let oauthConfig: SystemConfigOAuthDto;
|
||||
|
||||
let savedConfig: SystemConfigOAuthDto;
|
||||
let defaultConfig: SystemConfigOAuthDto;
|
||||
export let oauthDefault: SystemConfigOAuthDto;
|
||||
export let savedConfig: SystemConfigOAuthDto;
|
||||
|
||||
const handleToggleOverride = () => {
|
||||
// click runs before bind
|
||||
|
@ -25,13 +25,6 @@
|
|||
}
|
||||
};
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.oauth),
|
||||
api.systemConfigApi.getDefaults().then((res) => res.data.oauth),
|
||||
]);
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
|
@ -59,9 +52,7 @@
|
|||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: current } = await api.systemConfigApi.getConfig();
|
||||
|
||||
if (!current.passwordLogin.enabled && current.oauth.enabled && !oauthConfig.enabled) {
|
||||
if (!config.passwordLogin.enabled && config.oauth.enabled && !oauthConfig.enabled) {
|
||||
const confirmed = await openConfirmModal();
|
||||
if (!confirmed) {
|
||||
return;
|
||||
|
@ -72,15 +63,16 @@
|
|||
oauthConfig.mobileRedirectUri = '';
|
||||
}
|
||||
|
||||
const { data: updated } = await api.systemConfigApi.updateConfig({
|
||||
const { data } = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...current,
|
||||
...config,
|
||||
oauth: oauthConfig,
|
||||
},
|
||||
});
|
||||
|
||||
oauthConfig = { ...updated.oauth };
|
||||
savedConfig = { ...updated.oauth };
|
||||
oauthConfig = { ...data.oauth };
|
||||
savedConfig = { ...data.oauth };
|
||||
config = { ...data };
|
||||
|
||||
notificationController.show({ message: 'OAuth settings saved', type: NotificationType.Info });
|
||||
} catch (error) {
|
||||
|
@ -89,9 +81,7 @@
|
|||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: defaultConfig } = await api.systemConfigApi.getDefaults();
|
||||
|
||||
oauthConfig = { ...defaultConfig.oauth };
|
||||
oauthConfig = oauthDefault;
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset OAuth settings to default',
|
||||
|
@ -105,115 +95,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="http://immich.app/docs/administration/oauth#mobile-redirect-uri"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer">docs</a
|
||||
>.
|
||||
</p>
|
||||
<div in:fade={{ duration: 300 }}>
|
||||
<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="http://immich.app/docs/administration/oauth#mobile-redirect-uri"
|
||||
class="underline"
|
||||
target="_blank"
|
||||
rel="noreferrer">docs</a
|
||||
>.
|
||||
</p>
|
||||
|
||||
<SettingSwitch title="ENABLE" bind:checked={oauthConfig.enabled} />
|
||||
<hr />
|
||||
<SettingSwitch title="ENABLE" bind:checked={oauthConfig.enabled} />
|
||||
<hr />
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="ISSUER URL"
|
||||
bind:value={oauthConfig.issuerUrl}
|
||||
required={true}
|
||||
disabled={!oauthConfig.enabled}
|
||||
isEdited={!(oauthConfig.issuerUrl == savedConfig.issuerUrl)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="CLIENT ID"
|
||||
bind:value={oauthConfig.clientId}
|
||||
required={true}
|
||||
disabled={!oauthConfig.enabled}
|
||||
isEdited={!(oauthConfig.clientId == savedConfig.clientId)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="CLIENT SECRET"
|
||||
bind:value={oauthConfig.clientSecret}
|
||||
required={true}
|
||||
disabled={!oauthConfig.enabled}
|
||||
isEdited={!(oauthConfig.clientSecret == savedConfig.clientSecret)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="SCOPE"
|
||||
bind:value={oauthConfig.scope}
|
||||
required={true}
|
||||
disabled={!oauthConfig.enabled}
|
||||
isEdited={!(oauthConfig.scope == savedConfig.scope)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="STORAGE LABEL CLAIM"
|
||||
desc="Automatically set the user's storage label to the value of this claim."
|
||||
bind:value={oauthConfig.storageLabelClaim}
|
||||
required={true}
|
||||
disabled={!oauthConfig.storageLabelClaim}
|
||||
isEdited={!(oauthConfig.storageLabelClaim == savedConfig.storageLabelClaim)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="BUTTON TEXT"
|
||||
bind:value={oauthConfig.buttonText}
|
||||
required={false}
|
||||
disabled={!oauthConfig.enabled}
|
||||
isEdited={!(oauthConfig.buttonText == savedConfig.buttonText)}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="AUTO REGISTER"
|
||||
subtitle="Automatically register new users after signing in with OAuth"
|
||||
bind:checked={oauthConfig.autoRegister}
|
||||
disabled={!oauthConfig.enabled}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="AUTO LAUNCH"
|
||||
subtitle="Start the OAuth login flow automatically upon navigating to the login page"
|
||||
disabled={!oauthConfig.enabled}
|
||||
bind:checked={oauthConfig.autoLaunch}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="MOBILE REDIRECT URI OVERRIDE"
|
||||
subtitle="Enable when `app.immich:/` is an invalid redirect URI."
|
||||
disabled={!oauthConfig.enabled}
|
||||
on:click={() => handleToggleOverride()}
|
||||
bind:checked={oauthConfig.mobileOverrideEnabled}
|
||||
/>
|
||||
|
||||
{#if oauthConfig.mobileOverrideEnabled}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="ISSUER URL"
|
||||
bind:value={oauthConfig.issuerUrl}
|
||||
label="MOBILE REDIRECT URI"
|
||||
bind:value={oauthConfig.mobileRedirectUri}
|
||||
required={true}
|
||||
disabled={!oauthConfig.enabled}
|
||||
isEdited={!(oauthConfig.issuerUrl == savedConfig.issuerUrl)}
|
||||
isEdited={!(oauthConfig.mobileRedirectUri == savedConfig.mobileRedirectUri)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="CLIENT ID"
|
||||
bind:value={oauthConfig.clientId}
|
||||
required={true}
|
||||
disabled={!oauthConfig.enabled}
|
||||
isEdited={!(oauthConfig.clientId == savedConfig.clientId)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="CLIENT SECRET"
|
||||
bind:value={oauthConfig.clientSecret}
|
||||
required={true}
|
||||
disabled={!oauthConfig.enabled}
|
||||
isEdited={!(oauthConfig.clientSecret == savedConfig.clientSecret)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="SCOPE"
|
||||
bind:value={oauthConfig.scope}
|
||||
required={true}
|
||||
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={!oauthConfig.storageLabelClaim}
|
||||
isEdited={!(oauthConfig.storageLabelClaim == savedConfig.storageLabelClaim)}
|
||||
/>
|
||||
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="BUTTON TEXT"
|
||||
bind:value={oauthConfig.buttonText}
|
||||
required={false}
|
||||
disabled={!oauthConfig.enabled}
|
||||
isEdited={!(oauthConfig.buttonText == savedConfig.buttonText)}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="AUTO REGISTER"
|
||||
subtitle="Automatically register new users after signing in with OAuth"
|
||||
bind:checked={oauthConfig.autoRegister}
|
||||
disabled={!oauthConfig.enabled}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="AUTO LAUNCH"
|
||||
subtitle="Start the OAuth login flow automatically upon navigating to the login page"
|
||||
disabled={!oauthConfig.enabled}
|
||||
bind:checked={oauthConfig.autoLaunch}
|
||||
/>
|
||||
|
||||
<SettingSwitch
|
||||
title="MOBILE REDIRECT URI OVERRIDE"
|
||||
subtitle="Enable when `app.immich:/` is an invalid redirect URI."
|
||||
disabled={!oauthConfig.enabled}
|
||||
on:click={() => handleToggleOverride()}
|
||||
bind:checked={oauthConfig.mobileOverrideEnabled}
|
||||
/>
|
||||
|
||||
{#if oauthConfig.mobileOverrideEnabled}
|
||||
<SettingInputField
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
label="MOBILE REDIRECT URI"
|
||||
bind:value={oauthConfig.mobileRedirectUri}
|
||||
required={true}
|
||||
disabled={!oauthConfig.enabled}
|
||||
isEdited={!(oauthConfig.mobileRedirectUri == savedConfig.mobileRedirectUri)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(oauthConfig, oauthDefault)}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -4,24 +4,17 @@
|
|||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { api, SystemConfigPasswordLoginDto } from '@api';
|
||||
import { api, SystemConfigDto, SystemConfigPasswordLoginDto } from '@api';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { fade } from 'svelte/transition';
|
||||
import ConfirmDisableLogin from '../confirm-disable-login.svelte';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import SettingSwitch from '../setting-switch.svelte';
|
||||
|
||||
export let config: SystemConfigDto;
|
||||
export let passwordLoginConfig: SystemConfigPasswordLoginDto; // this is the config that is being edited
|
||||
|
||||
let savedConfig: SystemConfigPasswordLoginDto;
|
||||
let defaultConfig: SystemConfigPasswordLoginDto;
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.passwordLogin),
|
||||
api.systemConfigApi.getDefaults().then((res) => res.data.passwordLogin),
|
||||
]);
|
||||
}
|
||||
export let passwordLoginDefault: SystemConfigPasswordLoginDto;
|
||||
export let savedConfig: SystemConfigPasswordLoginDto;
|
||||
|
||||
let isConfirmOpen = false;
|
||||
let handleConfirm: (value: boolean) => void;
|
||||
|
@ -38,24 +31,23 @@
|
|||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: current } = await api.systemConfigApi.getConfig();
|
||||
|
||||
if (!current.oauth.enabled && current.passwordLogin.enabled && !passwordLoginConfig.enabled) {
|
||||
if (!config.oauth.enabled && config.passwordLogin.enabled && !passwordLoginConfig.enabled) {
|
||||
const confirmed = await openConfirmModal();
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const { data: updated } = await api.systemConfigApi.updateConfig({
|
||||
const { data } = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...current,
|
||||
...config,
|
||||
passwordLogin: passwordLoginConfig,
|
||||
},
|
||||
});
|
||||
|
||||
passwordLoginConfig = { ...updated.passwordLogin };
|
||||
savedConfig = { ...updated.passwordLogin };
|
||||
passwordLoginConfig = { ...data.passwordLogin };
|
||||
savedConfig = { ...data.passwordLogin };
|
||||
config = { ...data };
|
||||
|
||||
notificationController.show({ message: 'Settings saved', type: NotificationType.Info });
|
||||
} catch (error) {
|
||||
|
@ -64,10 +56,7 @@
|
|||
}
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
passwordLoginConfig = { ...resetConfig.passwordLogin };
|
||||
savedConfig = { ...resetConfig.passwordLogin };
|
||||
passwordLoginConfig = { ...savedConfig };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset settings to the recent saved settings',
|
||||
|
@ -76,10 +65,7 @@
|
|||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getDefaults();
|
||||
|
||||
passwordLoginConfig = { ...configs.passwordLogin };
|
||||
defaultConfig = { ...configs.passwordLogin };
|
||||
passwordLoginConfig = { ...passwordLoginDefault };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset password settings to default',
|
||||
|
@ -93,26 +79,25 @@
|
|||
{/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"
|
||||
subtitle="Login with email and password"
|
||||
bind:checked={passwordLoginConfig.enabled}
|
||||
/>
|
||||
<div in:fade={{ duration: 300 }}>
|
||||
<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="Login with email and password"
|
||||
isEdited={!(passwordLoginConfig.enabled == savedConfig.enabled)}
|
||||
bind:checked={passwordLoginConfig.enabled}
|
||||
/>
|
||||
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
/>
|
||||
</div>
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(passwordLoginConfig, passwordLoginDefault)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
<script lang="ts">
|
||||
import { api, SystemConfigStorageTemplateDto, SystemConfigTemplateStorageOptionDto, UserResponseDto } from '@api';
|
||||
import {
|
||||
api,
|
||||
SystemConfigDto,
|
||||
SystemConfigStorageTemplateDto,
|
||||
SystemConfigTemplateStorageOptionDto,
|
||||
UserResponseDto,
|
||||
} from '@api';
|
||||
import * as luxon from 'luxon';
|
||||
import handlebar from 'handlebars';
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import SupportedDatetimePanel from './supported-datetime-panel.svelte';
|
||||
import SupportedVariablesPanel from './supported-variables-panel.svelte';
|
||||
import SettingButtonsRow from '../setting-buttons-row.svelte';
|
||||
import { isEqual } from 'lodash-es';
|
||||
|
@ -14,29 +17,15 @@
|
|||
} from '$lib/components/shared-components/notification/notification';
|
||||
import SettingInputField, { SettingInputFieldType } from '../setting-input-field.svelte';
|
||||
|
||||
export let config: SystemConfigDto;
|
||||
export let storageConfig: SystemConfigStorageTemplateDto;
|
||||
export let storageDefault: SystemConfigStorageTemplateDto;
|
||||
export let user: UserResponseDto;
|
||||
export let templateOptions: SystemConfigTemplateStorageOptionDto;
|
||||
|
||||
let savedConfig: SystemConfigStorageTemplateDto;
|
||||
let defaultConfig: SystemConfigStorageTemplateDto;
|
||||
let templateOptions: SystemConfigTemplateStorageOptionDto;
|
||||
export let savedConfig: SystemConfigStorageTemplateDto;
|
||||
let selectedPreset = '';
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig, templateOptions] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.storageTemplate),
|
||||
api.systemConfigApi.getDefaults().then((res) => res.data.storageTemplate),
|
||||
api.systemConfigApi.getStorageTemplateOptions().then((res) => res.data),
|
||||
]);
|
||||
|
||||
selectedPreset = savedConfig.template;
|
||||
}
|
||||
|
||||
const getSupportDateTimeFormat = async () => {
|
||||
const { data } = await api.systemConfigApi.getStorageTemplateOptions();
|
||||
return data;
|
||||
};
|
||||
|
||||
$: parsedTemplate = () => {
|
||||
try {
|
||||
return renderTemplate(storageConfig.template);
|
||||
|
@ -76,11 +65,7 @@
|
|||
};
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
storageConfig.template = resetConfig.storageTemplate.template;
|
||||
savedConfig.template = resetConfig.storageTemplate.template;
|
||||
|
||||
storageConfig = { ...savedConfig };
|
||||
notificationController.show({
|
||||
message: 'Reset storage template settings to the recent saved settings',
|
||||
type: NotificationType.Info,
|
||||
|
@ -89,17 +74,16 @@
|
|||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: currentConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
const result = await api.systemConfigApi.updateConfig({
|
||||
const { data } = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...currentConfig,
|
||||
...config,
|
||||
storageTemplate: storageConfig,
|
||||
},
|
||||
});
|
||||
|
||||
storageConfig.template = result.data.storageTemplate.template;
|
||||
savedConfig.template = result.data.storageTemplate.template;
|
||||
storageConfig = { ...data.storageTemplate };
|
||||
savedConfig = { ...data.storageTemplate };
|
||||
config = { ...data };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Storage template saved',
|
||||
|
@ -115,9 +99,7 @@
|
|||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: defaultConfig } = await api.systemConfigApi.getDefaults();
|
||||
|
||||
storageConfig.template = defaultConfig.storageTemplate.template;
|
||||
storageConfig = { ...storageDefault };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset storage template to default',
|
||||
|
@ -131,94 +113,82 @@
|
|||
</script>
|
||||
|
||||
<section class="dark:text-immich-dark-fg">
|
||||
{#await getConfigs() then}
|
||||
<div id="directory-path-builder" class="m-4">
|
||||
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">Variables</h3>
|
||||
<div id="directory-path-builder" class="m-4">
|
||||
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">Variables</h3>
|
||||
|
||||
<section class="support-date">
|
||||
{#await getSupportDateTimeFormat()}
|
||||
<LoadingSpinner />
|
||||
{:then options}
|
||||
<div transition:fade={{ duration: 200 }}>
|
||||
<SupportedDatetimePanel {options} />
|
||||
<section class="support-date" />
|
||||
|
||||
<section class="support-date">
|
||||
<SupportedVariablesPanel />
|
||||
</section>
|
||||
|
||||
<div class="mt-4 flex flex-col">
|
||||
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">Template</h3>
|
||||
|
||||
<div class="my-2 text-xs">
|
||||
<h4>PREVIEW</h4>
|
||||
</div>
|
||||
|
||||
<p class="text-xs">
|
||||
Approximately path length limit : <span class="font-semibold text-immich-primary dark:text-immich-dark-primary"
|
||||
>{parsedTemplate().length + user.id.length + 'UPLOAD_LOCATION'.length}</span
|
||||
>/260
|
||||
</p>
|
||||
|
||||
<p class="text-xs">
|
||||
<code>{user.storageLabel || user.id}</code> is the user's Storage Label
|
||||
</p>
|
||||
|
||||
<p class="mt-2 rounded-lg bg-gray-200 p-4 py-2 text-xs dark:bg-gray-700 dark:text-immich-dark-fg">
|
||||
<span class="text-immich-fg/25 dark:text-immich-dark-fg/50">UPLOAD_LOCATION/{user.storageLabel || user.id}</span
|
||||
>/{parsedTemplate()}.jpg
|
||||
</p>
|
||||
|
||||
<form autocomplete="off" class="flex flex-col" on:submit|preventDefault>
|
||||
<div class="my-2 flex flex-col">
|
||||
<label class="text-xs" for="presets">PRESET</label>
|
||||
<select
|
||||
class="mt-2 rounded-lg bg-slate-200 p-2 text-sm hover:cursor-pointer dark:bg-gray-600"
|
||||
name="presets"
|
||||
id="preset-select"
|
||||
bind:value={selectedPreset}
|
||||
on:change={handlePresetSelection}
|
||||
>
|
||||
{#each templateOptions.presetOptions as preset}
|
||||
<option value={preset}>{renderTemplate(preset)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex gap-2 align-bottom">
|
||||
<SettingInputField
|
||||
label="TEMPLATE"
|
||||
required
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
bind:value={storageConfig.template}
|
||||
isEdited={!(storageConfig.template === savedConfig.template)}
|
||||
/>
|
||||
|
||||
<div class="flex-0">
|
||||
<SettingInputField label="EXTENSION" inputType={SettingInputFieldType.TEXT} value={'.jpg'} disabled />
|
||||
</div>
|
||||
{/await}
|
||||
</section>
|
||||
|
||||
<section class="support-date">
|
||||
<SupportedVariablesPanel />
|
||||
</section>
|
||||
|
||||
<div class="mt-4 flex flex-col">
|
||||
<h3 class="text-base font-medium text-immich-primary dark:text-immich-dark-primary">Template</h3>
|
||||
|
||||
<div class="my-2 text-xs">
|
||||
<h4>PREVIEW</h4>
|
||||
</div>
|
||||
|
||||
<p class="text-xs">
|
||||
Approximately path length limit : <span
|
||||
class="font-semibold text-immich-primary dark:text-immich-dark-primary"
|
||||
>{parsedTemplate().length + user.id.length + 'UPLOAD_LOCATION'.length}</span
|
||||
>/260
|
||||
</p>
|
||||
|
||||
<p class="text-xs">
|
||||
<code>{user.storageLabel || user.id}</code> is the user's Storage Label
|
||||
</p>
|
||||
|
||||
<p class="mt-2 rounded-lg bg-gray-200 p-4 py-2 text-xs dark:bg-gray-700 dark:text-immich-dark-fg">
|
||||
<span class="text-immich-fg/25 dark:text-immich-dark-fg/50"
|
||||
>UPLOAD_LOCATION/{user.storageLabel || user.id}</span
|
||||
>/{parsedTemplate()}.jpg
|
||||
</p>
|
||||
|
||||
<form autocomplete="off" class="flex flex-col" on:submit|preventDefault>
|
||||
<div class="my-2 flex flex-col">
|
||||
<label class="text-xs" for="presets">PRESET</label>
|
||||
<select
|
||||
class="mt-2 rounded-lg bg-slate-200 p-2 text-sm hover:cursor-pointer dark:bg-gray-600"
|
||||
name="presets"
|
||||
id="preset-select"
|
||||
bind:value={selectedPreset}
|
||||
on:change={handlePresetSelection}
|
||||
<div id="migration-info" class="mt-4 text-sm">
|
||||
<p>
|
||||
Template changes will only apply to new assets. To retroactively apply the template to previously uploaded
|
||||
assets, run the <a href="/admin/jobs-status" class="text-immich-primary dark:text-immich-dark-primary"
|
||||
>Storage Migration Job</a
|
||||
>
|
||||
{#each templateOptions.presetOptions as preset}
|
||||
<option value={preset}>{renderTemplate(preset)}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex gap-2 align-bottom">
|
||||
<SettingInputField
|
||||
label="TEMPLATE"
|
||||
required
|
||||
inputType={SettingInputFieldType.TEXT}
|
||||
bind:value={storageConfig.template}
|
||||
isEdited={!(storageConfig.template === savedConfig.template)}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex-0">
|
||||
<SettingInputField label="EXTENSION" inputType={SettingInputFieldType.TEXT} value={'.jpg'} disabled />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="migration-info" class="mt-4 text-sm">
|
||||
<p>
|
||||
Template changes will only apply to new assets. To retroactively apply the template to previously uploaded
|
||||
assets, run the <a href="/admin/jobs-status" class="text-immich-primary dark:text-immich-dark-primary"
|
||||
>Storage Migration Job</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(savedConfig, storageConfig)}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<script lang="ts">
|
||||
import SettingSelect from '$lib/components/admin-page/settings/setting-select.svelte';
|
||||
import { api, SystemConfigThumbnailDto } from '@api';
|
||||
import { api, SystemConfigDto, SystemConfigThumbnailDto } from '@api';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import SettingButtonsRow from '$lib/components/admin-page/settings/setting-buttons-row.svelte';
|
||||
|
@ -9,23 +9,14 @@
|
|||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
|
||||
export let config: SystemConfigDto;
|
||||
|
||||
export let thumbnailConfig: SystemConfigThumbnailDto; // this is the config that is being edited
|
||||
|
||||
let savedConfig: SystemConfigThumbnailDto;
|
||||
let defaultConfig: SystemConfigThumbnailDto;
|
||||
|
||||
async function getConfigs() {
|
||||
[savedConfig, defaultConfig] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data.thumbnail),
|
||||
api.systemConfigApi.getDefaults().then((res) => res.data.thumbnail),
|
||||
]);
|
||||
}
|
||||
export let thumbnailDefault: SystemConfigThumbnailDto;
|
||||
export let savedConfig: SystemConfigThumbnailDto;
|
||||
|
||||
async function reset() {
|
||||
const { data: resetConfig } = await api.systemConfigApi.getConfig();
|
||||
|
||||
thumbnailConfig = { ...resetConfig.thumbnail };
|
||||
savedConfig = { ...resetConfig.thumbnail };
|
||||
thumbnailConfig = { ...savedConfig };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset thumbnail settings to the recent saved settings',
|
||||
|
@ -34,10 +25,7 @@
|
|||
}
|
||||
|
||||
async function resetToDefault() {
|
||||
const { data: configs } = await api.systemConfigApi.getDefaults();
|
||||
|
||||
thumbnailConfig = { ...configs.thumbnail };
|
||||
defaultConfig = { ...configs.thumbnail };
|
||||
thumbnailConfig = { ...thumbnailDefault };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Reset thumbnail settings to default',
|
||||
|
@ -47,17 +35,16 @@
|
|||
|
||||
async function saveSetting() {
|
||||
try {
|
||||
const { data: configs } = await api.systemConfigApi.getConfig();
|
||||
|
||||
const result = await api.systemConfigApi.updateConfig({
|
||||
const { data } = await api.systemConfigApi.updateConfig({
|
||||
systemConfigDto: {
|
||||
...configs,
|
||||
...config,
|
||||
thumbnail: thumbnailConfig,
|
||||
},
|
||||
});
|
||||
|
||||
thumbnailConfig = { ...result.data.thumbnail };
|
||||
savedConfig = { ...result.data.thumbnail };
|
||||
thumbnailConfig = { ...data.thumbnail };
|
||||
savedConfig = { ...data.thumbnail };
|
||||
config = { ...data };
|
||||
|
||||
notificationController.show({
|
||||
message: 'Thumbnail settings saved',
|
||||
|
@ -74,48 +61,46 @@
|
|||
</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="WEBP RESOLUTION"
|
||||
desc="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' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={!(thumbnailConfig.webpSize === savedConfig.webpSize)}
|
||||
/>
|
||||
<div in:fade={{ duration: 300 }}>
|
||||
<form autocomplete="off" on:submit|preventDefault>
|
||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
||||
<SettingSelect
|
||||
label="WEBP RESOLUTION"
|
||||
desc="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' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={!(thumbnailConfig.webpSize === savedConfig.webpSize)}
|
||||
/>
|
||||
|
||||
<SettingSelect
|
||||
label="JPEG RESOLUTION"
|
||||
desc="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' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={!(thumbnailConfig.jpegSize === savedConfig.jpegSize)}
|
||||
/>
|
||||
</div>
|
||||
<SettingSelect
|
||||
label="JPEG RESOLUTION"
|
||||
desc="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' },
|
||||
]}
|
||||
name="resolution"
|
||||
isEdited={!(thumbnailConfig.jpegSize === savedConfig.jpegSize)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/await}
|
||||
<div class="ml-4">
|
||||
<SettingButtonsRow
|
||||
on:reset={reset}
|
||||
on:save={saveSetting}
|
||||
on:reset-to-default={resetToDefault}
|
||||
showResetToDefault={!isEqual(thumbnailConfig, thumbnailDefault)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -8,30 +8,51 @@
|
|||
import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte';
|
||||
import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte';
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
import { api } from '@api';
|
||||
import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, api } from '@api';
|
||||
import type { PageData } from './$types';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let config: SystemConfigDto;
|
||||
let currentConfig: SystemConfigDto;
|
||||
let defaultConfig: SystemConfigDto;
|
||||
let templateOptions: SystemConfigTemplateStorageOptionDto;
|
||||
const getConfig = async () => {
|
||||
const { data } = await api.systemConfigApi.getConfig();
|
||||
return data;
|
||||
[config, defaultConfig, templateOptions] = await Promise.all([
|
||||
api.systemConfigApi.getConfig().then((res) => res.data),
|
||||
api.systemConfigApi.getDefaults().then((res) => res.data),
|
||||
api.systemConfigApi.getStorageTemplateOptions().then((res) => res.data),
|
||||
]);
|
||||
|
||||
// deep copy
|
||||
currentConfig = JSON.parse(JSON.stringify(config));
|
||||
};
|
||||
</script>
|
||||
|
||||
<section class="">
|
||||
{#await getConfig()}
|
||||
<LoadingSpinner />
|
||||
{:then configs}
|
||||
<div class="flex items-center justify-center">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{:then}
|
||||
<SettingAccordion title="Thumbnail Settings" subtitle="Manage the resolution of thumbnail sizes">
|
||||
<ThumbnailSettings thumbnailConfig={configs.thumbnail} />
|
||||
<ThumbnailSettings
|
||||
savedConfig={currentConfig.thumbnail}
|
||||
bind:config={currentConfig}
|
||||
thumbnailConfig={config.thumbnail}
|
||||
thumbnailDefault={defaultConfig.thumbnail}
|
||||
/>
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
title="FFmpeg Settings"
|
||||
subtitle="Manage the resolution and encoding information of the video files"
|
||||
>
|
||||
<FFmpegSettings ffmpegConfig={configs.ffmpeg} />
|
||||
<FFmpegSettings
|
||||
savedConfig={currentConfig.ffmpeg}
|
||||
bind:config={currentConfig}
|
||||
ffmpegConfig={config.ffmpeg}
|
||||
ffmpegDefault={defaultConfig.ffmpeg}
|
||||
/>
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
|
@ -39,15 +60,25 @@
|
|||
subtitle="Manage job concurrency"
|
||||
isOpen={$page.url.searchParams.get('open') === 'job-settings'}
|
||||
>
|
||||
<JobSettings jobConfig={configs.job} />
|
||||
<JobSettings savedConfig={currentConfig.job} bind:config jobConfig={config.job} jobDefault={defaultConfig.job} />
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="Password Authentication" subtitle="Manage login with password settings">
|
||||
<PasswordLoginSettings passwordLoginConfig={configs.passwordLogin} />
|
||||
<PasswordLoginSettings
|
||||
savedConfig={currentConfig.passwordLogin}
|
||||
bind:config={currentConfig}
|
||||
passwordLoginConfig={config.passwordLogin}
|
||||
passwordLoginDefault={defaultConfig.passwordLogin}
|
||||
/>
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion title="OAuth Authentication" subtitle="Manage the login with OAuth settings">
|
||||
<OAuthSettings oauthConfig={configs.oauth} />
|
||||
<OAuthSettings
|
||||
savedConfig={currentConfig.oauth}
|
||||
bind:config={currentConfig}
|
||||
oauthConfig={config.oauth}
|
||||
oauthDefault={defaultConfig.oauth}
|
||||
/>
|
||||
</SettingAccordion>
|
||||
|
||||
<SettingAccordion
|
||||
|
@ -55,7 +86,14 @@
|
|||
subtitle="Manage the folder structure and file name of the upload asset"
|
||||
isOpen={$page.url.searchParams.get('open') === 'storage-template'}
|
||||
>
|
||||
<StorageTemplateSettings storageConfig={configs.storageTemplate} user={data.user} />
|
||||
<StorageTemplateSettings
|
||||
savedConfig={currentConfig.storageTemplate}
|
||||
bind:config={currentConfig}
|
||||
storageConfig={config.storageTemplate}
|
||||
user={data.user}
|
||||
storageDefault={defaultConfig.storageTemplate}
|
||||
{templateOptions}
|
||||
/>
|
||||
</SettingAccordion>
|
||||
{/await}
|
||||
</section>
|
||||
|
|
Loading…
Reference in a new issue