From 62a11283afb188db8e0db1bc2fb43cc395c0a547 Mon Sep 17 00:00:00 2001 From: Wingy Date: Mon, 23 Oct 2023 11:38:41 -0700 Subject: [PATCH] feat(web): custom stylesheets (#4602) * add initial ui and api definitions for stylesheets * proper saving * make custom css work * add textarea * rebuild api * run prettier * add typecast * update typings * move css accordion to be sorted alphabetically * set content-type properly * rename stylesheets to theme * fix server test --- cli/src/api/open-api/api.ts | 19 ++++ mobile/openapi/.openapi-generator/FILES | 3 + mobile/openapi/README.md | 1 + mobile/openapi/doc/SystemConfigDto.md | 1 + mobile/openapi/doc/SystemConfigThemeDto.md | 15 +++ mobile/openapi/lib/api.dart | 1 + mobile/openapi/lib/api_client.dart | 2 + .../openapi/lib/model/system_config_dto.dart | 10 +- .../lib/model/system_config_theme_dto.dart | 98 +++++++++++++++++++ .../openapi/test/system_config_dto_test.dart | 5 + .../test/system_config_theme_dto_test.dart | 27 +++++ server/immich-openapi-specs.json | 17 +++- .../dto/system-config-theme.dto.ts | 6 ++ .../system-config/dto/system-config.dto.ts | 6 ++ .../system-config/system-config.core.ts | 3 + .../system-config.service.spec.ts | 3 + .../infra/entities/system-config.entity.ts | 5 + web/src/api/open-api/api.ts | 19 ++++ .../settings/setting-textarea.svelte | 53 ++++++++++ .../settings/theme/theme-settings.svelte | 98 +++++++++++++++++++ web/src/routes/+layout.svelte | 1 + .../routes/admin/system-settings/+page.svelte | 5 + web/src/routes/custom.css/+server.ts | 9 ++ 23 files changed, 405 insertions(+), 2 deletions(-) create mode 100644 mobile/openapi/doc/SystemConfigThemeDto.md create mode 100644 mobile/openapi/lib/model/system_config_theme_dto.dart create mode 100644 mobile/openapi/test/system_config_theme_dto_test.dart create mode 100644 server/src/domain/system-config/dto/system-config-theme.dto.ts create mode 100644 web/src/lib/components/admin-page/settings/setting-textarea.svelte create mode 100644 web/src/lib/components/admin-page/settings/theme/theme-settings.svelte create mode 100644 web/src/routes/custom.css/+server.ts diff --git a/cli/src/api/open-api/api.ts b/cli/src/api/open-api/api.ts index 8d278b44c..2133d0411 100644 --- a/cli/src/api/open-api/api.ts +++ b/cli/src/api/open-api/api.ts @@ -3307,6 +3307,12 @@ export interface SystemConfigDto { * @memberof SystemConfigDto */ 'storageTemplate': SystemConfigStorageTemplateDto; + /** + * + * @type {SystemConfigThemeDto} + * @memberof SystemConfigDto + */ + 'theme': SystemConfigThemeDto; /** * * @type {SystemConfigThumbnailDto} @@ -3741,6 +3747,19 @@ export interface SystemConfigTemplateStorageOptionDto { */ 'yearOptions': Array; } +/** + * + * @export + * @interface SystemConfigThemeDto + */ +export interface SystemConfigThemeDto { + /** + * + * @type {string} + * @memberof SystemConfigThemeDto + */ + 'customCss': string; +} /** * * @export diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index 85b96e647..747c435dd 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -135,6 +135,7 @@ doc/SystemConfigPasswordLoginDto.md doc/SystemConfigReverseGeocodingDto.md doc/SystemConfigStorageTemplateDto.md doc/SystemConfigTemplateStorageOptionDto.md +doc/SystemConfigThemeDto.md doc/SystemConfigThumbnailDto.md doc/SystemConfigTrashDto.md doc/TagApi.md @@ -302,6 +303,7 @@ lib/model/system_config_password_login_dto.dart lib/model/system_config_reverse_geocoding_dto.dart lib/model/system_config_storage_template_dto.dart lib/model/system_config_template_storage_option_dto.dart +lib/model/system_config_theme_dto.dart lib/model/system_config_thumbnail_dto.dart lib/model/system_config_trash_dto.dart lib/model/tag_response_dto.dart @@ -456,6 +458,7 @@ test/system_config_password_login_dto_test.dart test/system_config_reverse_geocoding_dto_test.dart test/system_config_storage_template_dto_test.dart test/system_config_template_storage_option_dto_test.dart +test/system_config_theme_dto_test.dart test/system_config_thumbnail_dto_test.dart test/system_config_trash_dto_test.dart test/tag_api_test.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 47d04b9bd..07d2cf402 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -318,6 +318,7 @@ Class | Method | HTTP request | Description - [SystemConfigReverseGeocodingDto](doc//SystemConfigReverseGeocodingDto.md) - [SystemConfigStorageTemplateDto](doc//SystemConfigStorageTemplateDto.md) - [SystemConfigTemplateStorageOptionDto](doc//SystemConfigTemplateStorageOptionDto.md) + - [SystemConfigThemeDto](doc//SystemConfigThemeDto.md) - [SystemConfigThumbnailDto](doc//SystemConfigThumbnailDto.md) - [SystemConfigTrashDto](doc//SystemConfigTrashDto.md) - [TagResponseDto](doc//TagResponseDto.md) diff --git a/mobile/openapi/doc/SystemConfigDto.md b/mobile/openapi/doc/SystemConfigDto.md index a5b8db773..d426bef34 100644 --- a/mobile/openapi/doc/SystemConfigDto.md +++ b/mobile/openapi/doc/SystemConfigDto.md @@ -16,6 +16,7 @@ Name | Type | Description | Notes **passwordLogin** | [**SystemConfigPasswordLoginDto**](SystemConfigPasswordLoginDto.md) | | **reverseGeocoding** | [**SystemConfigReverseGeocodingDto**](SystemConfigReverseGeocodingDto.md) | | **storageTemplate** | [**SystemConfigStorageTemplateDto**](SystemConfigStorageTemplateDto.md) | | +**theme** | [**SystemConfigThemeDto**](SystemConfigThemeDto.md) | | **thumbnail** | [**SystemConfigThumbnailDto**](SystemConfigThumbnailDto.md) | | **trash** | [**SystemConfigTrashDto**](SystemConfigTrashDto.md) | | diff --git a/mobile/openapi/doc/SystemConfigThemeDto.md b/mobile/openapi/doc/SystemConfigThemeDto.md new file mode 100644 index 000000000..bcdbb690c --- /dev/null +++ b/mobile/openapi/doc/SystemConfigThemeDto.md @@ -0,0 +1,15 @@ +# openapi.model.SystemConfigThemeDto + +## Load the model package +```dart +import 'package:openapi/api.dart'; +``` + +## Properties +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**customCss** | **String** | | + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index e72c1da16..091a38e36 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -163,6 +163,7 @@ part 'model/system_config_password_login_dto.dart'; part 'model/system_config_reverse_geocoding_dto.dart'; part 'model/system_config_storage_template_dto.dart'; part 'model/system_config_template_storage_option_dto.dart'; +part 'model/system_config_theme_dto.dart'; part 'model/system_config_thumbnail_dto.dart'; part 'model/system_config_trash_dto.dart'; part 'model/tag_response_dto.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 34b9a431d..33586a7e1 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -417,6 +417,8 @@ class ApiClient { return SystemConfigStorageTemplateDto.fromJson(value); case 'SystemConfigTemplateStorageOptionDto': return SystemConfigTemplateStorageOptionDto.fromJson(value); + case 'SystemConfigThemeDto': + return SystemConfigThemeDto.fromJson(value); case 'SystemConfigThumbnailDto': return SystemConfigThumbnailDto.fromJson(value); case 'SystemConfigTrashDto': diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart index 932e68466..8e15d7e21 100644 --- a/mobile/openapi/lib/model/system_config_dto.dart +++ b/mobile/openapi/lib/model/system_config_dto.dart @@ -21,6 +21,7 @@ class SystemConfigDto { required this.passwordLogin, required this.reverseGeocoding, required this.storageTemplate, + required this.theme, required this.thumbnail, required this.trash, }); @@ -41,6 +42,8 @@ class SystemConfigDto { SystemConfigStorageTemplateDto storageTemplate; + SystemConfigThemeDto theme; + SystemConfigThumbnailDto thumbnail; SystemConfigTrashDto trash; @@ -55,6 +58,7 @@ class SystemConfigDto { other.passwordLogin == passwordLogin && other.reverseGeocoding == reverseGeocoding && other.storageTemplate == storageTemplate && + other.theme == theme && other.thumbnail == thumbnail && other.trash == trash; @@ -69,11 +73,12 @@ class SystemConfigDto { (passwordLogin.hashCode) + (reverseGeocoding.hashCode) + (storageTemplate.hashCode) + + (theme.hashCode) + (thumbnail.hashCode) + (trash.hashCode); @override - String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, machineLearning=$machineLearning, map=$map, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, storageTemplate=$storageTemplate, thumbnail=$thumbnail, trash=$trash]'; + String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, job=$job, machineLearning=$machineLearning, map=$map, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, storageTemplate=$storageTemplate, theme=$theme, thumbnail=$thumbnail, trash=$trash]'; Map toJson() { final json = {}; @@ -85,6 +90,7 @@ class SystemConfigDto { json[r'passwordLogin'] = this.passwordLogin; json[r'reverseGeocoding'] = this.reverseGeocoding; json[r'storageTemplate'] = this.storageTemplate; + json[r'theme'] = this.theme; json[r'thumbnail'] = this.thumbnail; json[r'trash'] = this.trash; return json; @@ -106,6 +112,7 @@ class SystemConfigDto { passwordLogin: SystemConfigPasswordLoginDto.fromJson(json[r'passwordLogin'])!, reverseGeocoding: SystemConfigReverseGeocodingDto.fromJson(json[r'reverseGeocoding'])!, storageTemplate: SystemConfigStorageTemplateDto.fromJson(json[r'storageTemplate'])!, + theme: SystemConfigThemeDto.fromJson(json[r'theme'])!, thumbnail: SystemConfigThumbnailDto.fromJson(json[r'thumbnail'])!, trash: SystemConfigTrashDto.fromJson(json[r'trash'])!, ); @@ -163,6 +170,7 @@ class SystemConfigDto { 'passwordLogin', 'reverseGeocoding', 'storageTemplate', + 'theme', 'thumbnail', 'trash', }; diff --git a/mobile/openapi/lib/model/system_config_theme_dto.dart b/mobile/openapi/lib/model/system_config_theme_dto.dart new file mode 100644 index 000000000..f34234952 --- /dev/null +++ b/mobile/openapi/lib/model/system_config_theme_dto.dart @@ -0,0 +1,98 @@ +// +// 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 SystemConfigThemeDto { + /// Returns a new [SystemConfigThemeDto] instance. + SystemConfigThemeDto({ + required this.customCss, + }); + + String customCss; + + @override + bool operator ==(Object other) => identical(this, other) || other is SystemConfigThemeDto && + other.customCss == customCss; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (customCss.hashCode); + + @override + String toString() => 'SystemConfigThemeDto[customCss=$customCss]'; + + Map toJson() { + final json = {}; + json[r'customCss'] = this.customCss; + return json; + } + + /// Returns a new [SystemConfigThemeDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static SystemConfigThemeDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return SystemConfigThemeDto( + customCss: mapValueOfType(json, r'customCss')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = SystemConfigThemeDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = SystemConfigThemeDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of SystemConfigThemeDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = SystemConfigThemeDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'customCss', + }; +} + diff --git a/mobile/openapi/test/system_config_dto_test.dart b/mobile/openapi/test/system_config_dto_test.dart index 1a3e38a9e..30dbe6860 100644 --- a/mobile/openapi/test/system_config_dto_test.dart +++ b/mobile/openapi/test/system_config_dto_test.dart @@ -56,6 +56,11 @@ void main() { // TODO }); + // SystemConfigThemeDto theme + test('to test the property `theme`', () async { + // TODO + }); + // SystemConfigThumbnailDto thumbnail test('to test the property `thumbnail`', () async { // TODO diff --git a/mobile/openapi/test/system_config_theme_dto_test.dart b/mobile/openapi/test/system_config_theme_dto_test.dart new file mode 100644 index 000000000..98e283559 --- /dev/null +++ b/mobile/openapi/test/system_config_theme_dto_test.dart @@ -0,0 +1,27 @@ +// +// 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 SystemConfigThemeDto +void main() { + // final instance = SystemConfigThemeDto(); + + group('test SystemConfigThemeDto', () { + // String customCss + test('to test the property `customCss`', () async { + // TODO + }); + + + }); + +} diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index e6230fcc3..09ff41e24 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -8060,6 +8060,9 @@ "storageTemplate": { "$ref": "#/components/schemas/SystemConfigStorageTemplateDto" }, + "theme": { + "$ref": "#/components/schemas/SystemConfigThemeDto" + }, "thumbnail": { "$ref": "#/components/schemas/SystemConfigThumbnailDto" }, @@ -8077,7 +8080,8 @@ "storageTemplate", "job", "thumbnail", - "trash" + "trash", + "theme" ], "type": "object" }, @@ -8404,6 +8408,17 @@ ], "type": "object" }, + "SystemConfigThemeDto": { + "properties": { + "customCss": { + "type": "string" + } + }, + "required": [ + "customCss" + ], + "type": "object" + }, "SystemConfigThumbnailDto": { "properties": { "colorspace": { diff --git a/server/src/domain/system-config/dto/system-config-theme.dto.ts b/server/src/domain/system-config/dto/system-config-theme.dto.ts new file mode 100644 index 000000000..f47b51e0e --- /dev/null +++ b/server/src/domain/system-config/dto/system-config-theme.dto.ts @@ -0,0 +1,6 @@ +import { IsString } from 'class-validator'; + +export class SystemConfigThemeDto { + @IsString() + customCss!: string; +} diff --git a/server/src/domain/system-config/dto/system-config.dto.ts b/server/src/domain/system-config/dto/system-config.dto.ts index 152268382..6a88e758c 100644 --- a/server/src/domain/system-config/dto/system-config.dto.ts +++ b/server/src/domain/system-config/dto/system-config.dto.ts @@ -9,6 +9,7 @@ import { SystemConfigOAuthDto } from './system-config-oauth.dto'; import { SystemConfigPasswordLoginDto } from './system-config-password-login.dto'; import { SystemConfigReverseGeocodingDto } from './system-config-reverse-geocoding.dto'; import { SystemConfigStorageTemplateDto } from './system-config-storage-template.dto'; +import { SystemConfigThemeDto } from './system-config-theme.dto'; import { SystemConfigThumbnailDto } from './system-config-thumbnail.dto'; import { SystemConfigTrashDto } from './system-config-trash.dto'; @@ -62,6 +63,11 @@ export class SystemConfigDto implements SystemConfig { @ValidateNested() @IsObject() trash!: SystemConfigTrashDto; + + @Type(() => SystemConfigThemeDto) + @ValidateNested() + @IsObject() + theme!: SystemConfigThemeDto; } export function mapConfig(config: SystemConfig): SystemConfigDto { diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts index 66c72fc92..4fd2faa29 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/domain/system-config/system-config.core.ts @@ -114,6 +114,9 @@ export const defaults = Object.freeze({ enabled: true, days: 30, }, + theme: { + customCss: '', + }, }); export enum FeatureFlag { diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index 0444217c0..36cdb6543 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -115,6 +115,9 @@ const updatedConfig = Object.freeze({ enabled: true, days: 10, }, + theme: { + customCss: '', + }, }); describe(SystemConfigService.name, () => { diff --git a/server/src/infra/entities/system-config.entity.ts b/server/src/infra/entities/system-config.entity.ts index 47b1f69fd..6bd552111 100644 --- a/server/src/infra/entities/system-config.entity.ts +++ b/server/src/infra/entities/system-config.entity.ts @@ -90,6 +90,8 @@ export enum SystemConfigKey { TRASH_ENABLED = 'trash.enabled', TRASH_DAYS = 'trash.days', + + THEME_CUSTOM_CSS = 'theme.customCss', } export enum TranscodePolicy { @@ -221,4 +223,7 @@ export interface SystemConfig { enabled: boolean; days: number; }; + theme: { + customCss: string; + }; } diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 8d278b44c..2133d0411 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -3307,6 +3307,12 @@ export interface SystemConfigDto { * @memberof SystemConfigDto */ 'storageTemplate': SystemConfigStorageTemplateDto; + /** + * + * @type {SystemConfigThemeDto} + * @memberof SystemConfigDto + */ + 'theme': SystemConfigThemeDto; /** * * @type {SystemConfigThumbnailDto} @@ -3741,6 +3747,19 @@ export interface SystemConfigTemplateStorageOptionDto { */ 'yearOptions': Array; } +/** + * + * @export + * @interface SystemConfigThemeDto + */ +export interface SystemConfigThemeDto { + /** + * + * @type {string} + * @memberof SystemConfigThemeDto + */ + 'customCss': string; +} /** * * @export diff --git a/web/src/lib/components/admin-page/settings/setting-textarea.svelte b/web/src/lib/components/admin-page/settings/setting-textarea.svelte new file mode 100644 index 000000000..99a672c47 --- /dev/null +++ b/web/src/lib/components/admin-page/settings/setting-textarea.svelte @@ -0,0 +1,53 @@ + + +
+
+ + {#if required} +
*
+ {/if} + + {#if isEdited} +
+ Unsaved change +
+ {/if} +
+ + {#if desc} +

+ {desc} +

+ {:else} + + {/if} + +