0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2025-04-08 03:01:32 -05:00

feat(web/server): webp thumbnail size configurable (#3598)

* feat(server/web): webp thumbnail size configurable

* update api

* add ui and fix test

* lint

* setting for jpeg size

* feat: coerce to number

* api

* jpeg resolution

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
Alex 2023-08-08 09:39:51 -05:00 committed by GitHub
parent 1812e8811b
commit ddd4ec2d9e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 429 additions and 18 deletions

View file

@ -2425,6 +2425,12 @@ export interface SystemConfigDto {
* @memberof SystemConfigDto
*/
'storageTemplate': SystemConfigStorageTemplateDto;
/**
*
* @type {SystemConfigThumbnailDto}
* @memberof SystemConfigDto
*/
'thumbnail': SystemConfigThumbnailDto;
}
/**
*
@ -2716,6 +2722,25 @@ export interface SystemConfigTemplateStorageOptionDto {
*/
'yearOptions': Array<string>;
}
/**
*
* @export
* @interface SystemConfigThumbnailDto
*/
export interface SystemConfigThumbnailDto {
/**
*
* @type {number}
* @memberof SystemConfigThumbnailDto
*/
'jpegSize': number;
/**
*
* @type {number}
* @memberof SystemConfigThumbnailDto
*/
'webpSize': number;
}
/**
*
* @export

View file

@ -104,6 +104,7 @@ doc/SystemConfigOAuthDto.md
doc/SystemConfigPasswordLoginDto.md
doc/SystemConfigStorageTemplateDto.md
doc/SystemConfigTemplateStorageOptionDto.md
doc/SystemConfigThumbnailDto.md
doc/TagApi.md
doc/TagResponseDto.md
doc/TagTypeEnum.md
@ -236,6 +237,7 @@ lib/model/system_config_o_auth_dto.dart
lib/model/system_config_password_login_dto.dart
lib/model/system_config_storage_template_dto.dart
lib/model/system_config_template_storage_option_dto.dart
lib/model/system_config_thumbnail_dto.dart
lib/model/tag_response_dto.dart
lib/model/tag_type_enum.dart
lib/model/thumbnail_format.dart
@ -355,6 +357,7 @@ test/system_config_o_auth_dto_test.dart
test/system_config_password_login_dto_test.dart
test/system_config_storage_template_dto_test.dart
test/system_config_template_storage_option_dto_test.dart
test/system_config_thumbnail_dto_test.dart
test/tag_api_test.dart
test/tag_response_dto_test.dart
test/tag_type_enum_test.dart

View file

@ -267,6 +267,7 @@ Class | Method | HTTP request | Description
- [SystemConfigPasswordLoginDto](doc//SystemConfigPasswordLoginDto.md)
- [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md)
- [SystemConfigTemplateStorageOptionDto](doc//SystemConfigTemplateStorageOptionDto.md)
- [SystemConfigThumbnailDto](doc//SystemConfigThumbnailDto.md)
- [TagResponseDto](doc//TagResponseDto.md)
- [TagTypeEnum](doc//TagTypeEnum.md)
- [ThumbnailFormat](doc//ThumbnailFormat.md)

View file

@ -13,6 +13,7 @@ Name | Type | Description | Notes
**oauth** | [**SystemConfigOAuthDto**](SystemConfigOAuthDto.md) | |
**passwordLogin** | [**SystemConfigPasswordLoginDto**](SystemConfigPasswordLoginDto.md) | |
**storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) | |
**thumbnail** | [**SystemConfigThumbnailDto**](SystemConfigThumbnailDto.md) | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -0,0 +1,16 @@
# openapi.model.SystemConfigThumbnailDto
## Load the model package
```dart
import 'package:openapi/api.dart';
```
## Properties
Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
**jpegSize** | **int** | |
**webpSize** | **int** | |
[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md)

View file

@ -132,6 +132,7 @@ part 'model/system_config_o_auth_dto.dart';
part 'model/system_config_password_login_dto.dart';
part 'model/system_config_storage_template_dto.dart';
part 'model/system_config_template_storage_option_dto.dart';
part 'model/system_config_thumbnail_dto.dart';
part 'model/tag_response_dto.dart';
part 'model/tag_type_enum.dart';
part 'model/thumbnail_format.dart';

View file

@ -359,6 +359,8 @@ class ApiClient {
return SystemConfigStorageTemplateDto.fromJson(value);
case 'SystemConfigTemplateStorageOptionDto':
return SystemConfigTemplateStorageOptionDto.fromJson(value);
case 'SystemConfigThumbnailDto':
return SystemConfigThumbnailDto.fromJson(value);
case 'TagResponseDto':
return TagResponseDto.fromJson(value);
case 'TagTypeEnum':

View file

@ -18,6 +18,7 @@ class SystemConfigDto {
required this.oauth,
required this.passwordLogin,
required this.storageTemplate,
required this.thumbnail,
});
SystemConfigFFmpegDto ffmpeg;
@ -30,13 +31,16 @@ class SystemConfigDto {
SystemConfigStorageTemplateDto storageTemplate;
SystemConfigThumbnailDto thumbnail;
@override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigDto &&
other.ffmpeg == ffmpeg &&
other.job == job &&
other.oauth == oauth &&
other.passwordLogin == passwordLogin &&
other.storageTemplate == storageTemplate;
other.storageTemplate == storageTemplate &&
other.thumbnail == thumbnail;
@override
int get hashCode =>
@ -45,10 +49,11 @@ class SystemConfigDto {
(job.hashCode) +
(oauth.hashCode) +
(passwordLogin.hashCode) +
(storageTemplate.hashCode);
(storageTemplate.hashCode) +
(thumbnail.hashCode);
@override
String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, oauth=$oauth, passwordLogin=$passwordLogin, storageTemplate=$storageTemplate]';
String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, oauth=$oauth, passwordLogin=$passwordLogin, storageTemplate=$storageTemplate, thumbnail=$thumbnail]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@ -57,6 +62,7 @@ class SystemConfigDto {
json[r'oauth'] = this.oauth;
json[r'passwordLogin'] = this.passwordLogin;
json[r'storageTemplate'] = this.storageTemplate;
json[r'thumbnail'] = this.thumbnail;
return json;
}
@ -73,6 +79,7 @@ class SystemConfigDto {
oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!,
passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!,
storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!,
thumbnail: SystemConfigThumbnailDto.fromJson(json[r'thumbnail'])!,
);
}
return null;
@ -125,6 +132,7 @@ class SystemConfigDto {
'oauth',
'passwordLogin',
'storageTemplate',
'thumbnail',
};
}

View file

@ -0,0 +1,106 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class SystemConfigThumbnailDto {
/// Returns a new [SystemConfigThumbnailDto] instance.
SystemConfigThumbnailDto({
required this.jpegSize,
required this.webpSize,
});
int jpegSize;
int webpSize;
@override
bool operator ==(Object other) => identical(this, other) || other is SystemConfigThumbnailDto &&
other.jpegSize == jpegSize &&
other.webpSize == webpSize;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(jpegSize.hashCode) +
(webpSize.hashCode);
@override
String toString() => 'SystemConfigThumbnailDto[jpegSize=$jpegSize, webpSize=$webpSize]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'jpegSize'] = this.jpegSize;
json[r'webpSize'] = this.webpSize;
return json;
}
/// Returns a new [SystemConfigThumbnailDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SystemConfigThumbnailDto? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast<String, dynamic>();
return SystemConfigThumbnailDto(
jpegSize: mapValueOfType<int>(json, r'jpegSize')!,
webpSize: mapValueOfType<int>(json, r'webpSize')!,
);
}
return null;
}
static List<SystemConfigThumbnailDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SystemConfigThumbnailDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SystemConfigThumbnailDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SystemConfigThumbnailDto> mapFromJson(dynamic json) {
final map = <String, SystemConfigThumbnailDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SystemConfigThumbnailDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SystemConfigThumbnailDto-objects as value to a dart map
static Map<String, List<SystemConfigThumbnailDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SystemConfigThumbnailDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SystemConfigThumbnailDto.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'jpegSize',
'webpSize',
};
}

View file

@ -41,6 +41,11 @@ void main() {
// TODO
});
// SystemConfigThumbnailDto thumbnail
test('to test the property `thumbnail`', () async {
// TODO
});
});

View file

@ -0,0 +1,32 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.12
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
import 'package:openapi/api.dart';
import 'package:test/test.dart';
// tests for SystemConfigThumbnailDto
void main() {
// final instance = SystemConfigThumbnailDto();
group('test SystemConfigThumbnailDto', () {
// int jpegSize
test('to test the property `jpegSize`', () async {
// TODO
});
// int webpSize
test('to test the property `webpSize`', () async {
// TODO
});
});
}

View file

@ -6590,6 +6590,9 @@
},
"storageTemplate": {
"$ref": "#/components/schemas/SystemConfigStorageTemplateDto"
},
"thumbnail": {
"$ref": "#/components/schemas/SystemConfigThumbnailDto"
}
},
"required": [
@ -6597,7 +6600,8 @@
"oauth",
"passwordLogin",
"storageTemplate",
"job"
"job",
"thumbnail"
],
"type": "object"
},
@ -6828,6 +6832,21 @@
],
"type": "object"
},
"SystemConfigThumbnailDto": {
"properties": {
"jpegSize": {
"type": "integer"
},
"webpSize": {
"type": "integer"
}
},
"required": [
"webpSize",
"jpegSize"
],
"type": "object"
},
"TagResponseDto": {
"properties": {
"id": {

View file

@ -1,3 +1 @@
export const JPEG_THUMBNAIL_SIZE = 1440;
export const WEBP_THUMBNAIL_SIZE = 250;
export const FACE_THUMBNAIL_SIZE = 250;

View file

@ -7,7 +7,6 @@ import { IBaseJob, IEntityJob, IJobRepository, JobName, JOBS_ASSET_PAGINATION_SI
import { IStorageRepository, StorageCore, StorageFolder } from '../storage';
import { ISystemConfigRepository, SystemConfigFFmpegDto } from '../system-config';
import { SystemConfigCore } from '../system-config/system-config.core';
import { JPEG_THUMBNAIL_SIZE, WEBP_THUMBNAIL_SIZE } from './media.constant';
import { AudioStreamInfo, IMediaRepository, VideoCodecHWConfig, VideoStreamInfo } from './media.repository';
import { H264Config, HEVCConfig, NVENCConfig, QSVConfig, ThumbnailConfig, VAAPIConfig, VP9Config } from './media.util';
@ -63,11 +62,12 @@ export class MediaService {
const resizePath = this.storageCore.getFolderLocation(StorageFolder.THUMBNAILS, asset.ownerId);
this.storageRepository.mkdirSync(resizePath);
const jpegThumbnailPath = join(resizePath, `${asset.id}.jpeg`);
const { thumbnail } = await this.configCore.getConfig();
switch (asset.type) {
case AssetType.IMAGE:
await this.mediaRepository.resize(asset.originalPath, jpegThumbnailPath, {
size: JPEG_THUMBNAIL_SIZE,
size: thumbnail.jpegSize,
format: 'jpeg',
});
this.logger.log(`Successfully generated image thumbnail ${asset.id}`);
@ -80,7 +80,7 @@ export class MediaService {
return false;
}
const { ffmpeg } = await this.configCore.getConfig();
const config = { ...ffmpeg, targetResolution: JPEG_THUMBNAIL_SIZE.toString(), twoPass: false };
const config = { ...ffmpeg, targetResolution: thumbnail.jpegSize.toString(), twoPass: false };
const options = new ThumbnailConfig(config).getOptions(mainVideoStream);
await this.mediaRepository.transcode(asset.originalPath, jpegThumbnailPath, options);
this.logger.log(`Successfully generated video thumbnail ${asset.id}`);
@ -100,7 +100,8 @@ export class MediaService {
const webpPath = asset.resizePath.replace('jpeg', 'webp').replace('jpg', 'webp');
await this.mediaRepository.resize(asset.resizePath, webpPath, { size: WEBP_THUMBNAIL_SIZE, format: 'webp' });
const { thumbnail } = await this.configCore.getConfig();
await this.mediaRepository.resize(asset.resizePath, webpPath, { size: thumbnail.webpSize, format: 'webp' });
await this.assetRepository.save({ id: asset.id, webpPath });
return true;

View file

@ -2,4 +2,5 @@ export * from './system-config-ffmpeg.dto';
export * from './system-config-oauth.dto';
export * from './system-config-password-login.dto';
export * from './system-config-storage-template.dto';
export * from './system-config-thumbnail.dto';
export * from './system-config.dto';

View file

@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsInt } from 'class-validator';
export class SystemConfigThumbnailDto {
@IsInt()
@Type(() => Number)
@ApiProperty({ type: 'integer' })
webpSize!: number;
@IsInt()
@Type(() => Number)
@ApiProperty({ type: 'integer' })
jpegSize!: number;
}

View file

@ -1,3 +1,4 @@
import { SystemConfigThumbnailDto } from '@app/domain/system-config';
import { SystemConfig } from '@app/infra/entities';
import { Type } from 'class-transformer';
import { IsObject, ValidateNested } from 'class-validator';
@ -32,6 +33,11 @@ export class SystemConfigDto {
@ValidateNested()
@IsObject()
job!: SystemConfigJobDto;
@Type(() => SystemConfigThumbnailDto)
@ValidateNested()
@IsObject()
thumbnail!: SystemConfigThumbnailDto;
}
export function mapConfig(config: SystemConfig): SystemConfigDto {

View file

@ -64,6 +64,11 @@ export const defaults = Object.freeze<SystemConfig>({
storageTemplate: {
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
},
thumbnail: {
webpSize: 250,
jpegSize: 1440,
},
});
const singleton = new Subject<SystemConfig>();

View file

@ -65,6 +65,10 @@ const updatedConfig = Object.freeze<SystemConfig>({
storageTemplate: {
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
},
thumbnail: {
webpSize: 250,
jpegSize: 1440,
},
});
describe(SystemConfigService.name, () => {

View file

@ -52,6 +52,9 @@ export enum SystemConfigKey {
PASSWORD_LOGIN_ENABLED = 'passwordLogin.enabled',
STORAGE_TEMPLATE = 'storageTemplate.template',
THUMBNAIL_WEBP_SIZE = 'thumbnail.webpSize',
THUMBNAIL_JPEG_SIZE = 'thumbnail.jpegSize',
}
export enum TranscodePolicy {
@ -121,4 +124,8 @@ export interface SystemConfig {
storageTemplate: {
template: string;
};
thumbnail: {
webpSize: number;
jpegSize: number;
};
}

View file

@ -12,7 +12,7 @@ export class MediaRepository implements IMediaRepository {
private logger = new Logger(MediaRepository.name);
crop(input: string, options: CropOptions): Promise<Buffer> {
return sharp(input, { failOnError: false })
return sharp(input, { failOn: 'none' })
.extract({
left: options.left,
top: options.top,
@ -23,7 +23,7 @@ export class MediaRepository implements IMediaRepository {
}
async resize(input: string | Buffer, output: string, options: ResizeOptions): Promise<void> {
await sharp(input, { failOnError: false })
await sharp(input, { failOn: 'none' })
.resize(options.size, options.size, { fit: 'outside', withoutEnlargement: true })
.rotate()
.toFormat(options.format)

View file

@ -2425,6 +2425,12 @@ export interface SystemConfigDto {
* @memberof SystemConfigDto
*/
'storageTemplate': SystemConfigStorageTemplateDto;
/**
*
* @type {SystemConfigThumbnailDto}
* @memberof SystemConfigDto
*/
'thumbnail': SystemConfigThumbnailDto;
}
/**
*
@ -2716,6 +2722,25 @@ export interface SystemConfigTemplateStorageOptionDto {
*/
'yearOptions': Array<string>;
}
/**
*
* @export
* @interface SystemConfigThumbnailDto
*/
export interface SystemConfigThumbnailDto {
/**
*
* @type {number}
* @memberof SystemConfigThumbnailDto
*/
'jpegSize': number;
/**
*
* @type {number}
* @memberof SystemConfigThumbnailDto
*/
'webpSize': number;
}
/**
*
* @export

View file

@ -27,7 +27,7 @@
};
</script>
<div class="w-full">
<div class="mb-4 w-full">
<div class={`flex h-[26px] place-items-center gap-1`}>
<label class={`immich-form-label text-sm`} for={label}>{label}</label>
{#if required}
@ -45,7 +45,7 @@
</div>
{#if desc}
<p class="immich-form-label pb-2 text-xs" id="{label}-desc">
<p class="immich-form-label pb-2 text-sm" id="{label}-desc">
{desc}
</p>
{/if}

View file

@ -2,19 +2,23 @@
import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
export let value: string;
export let options: { value: string; text: string }[];
export let value: string | number;
export let options: { value: string | number; text: string }[];
export let label = '';
export let desc = '';
export let name = '';
export let isEdited = false;
export let number = false;
const handleChange = (e: Event) => {
value = (e.target as HTMLInputElement).value;
if (number) {
value = parseInt(value);
}
};
</script>
<div class="w-full">
<div class="mb-4 w-full">
<div class={`flex h-[26px] place-items-center gap-1`}>
<label class={`immich-form-label text-sm`} for="{name}-select">{label}</label>
@ -29,7 +33,7 @@
</div>
{#if desc}
<p class="immich-form-label pb-2 text-xs" id="{name}-desc">
<p class="immich-form-label pb-2 text-sm" id="{name}-desc">
{desc}
</p>
{/if}

View file

@ -0,0 +1,121 @@
<script lang="ts">
import SettingSelect from '$lib/components/admin-page/settings/setting-select.svelte';
import { api, 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';
import {
notificationController,
NotificationType,
} from '$lib/components/shared-components/notification/notification';
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),
]);
}
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.getDefaults();
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,
});
}
}
</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)}
/>
<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>

View file

@ -2,6 +2,7 @@
import { page } from '$app/stores';
import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte';
import JobSettings from '$lib/components/admin-page/settings/job-settings/job-settings.svelte';
import ThumbnailSettings from '$lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte';
import OAuthSettings from '$lib/components/admin-page/settings/oauth/oauth-settings.svelte';
import PasswordLoginSettings from '$lib/components/admin-page/settings/password-login/password-login-settings.svelte';
import SettingAccordion from '$lib/components/admin-page/settings/setting-accordion.svelte';
@ -22,6 +23,10 @@
{#await getConfig()}
<LoadingSpinner />
{:then configs}
<SettingAccordion title="Thumbnail Settings" subtitle="Manage the resolution of thumbnail sizes">
<ThumbnailSettings thumbnailConfig={configs.thumbnail} />
</SettingAccordion>
<SettingAccordion
title="FFmpeg Settings"
subtitle="Manage the resolution and encoding information of the video files"