1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

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
This commit is contained in:
Wingy 2023-10-23 11:38:41 -07:00 committed by GitHub
parent 28d35bf04e
commit 62a11283af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 246 additions and 1 deletions

View File

@ -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<string>;
}
/**
*
* @export
* @interface SystemConfigThemeDto
*/
export interface SystemConfigThemeDto {
/**
*
* @type {string}
* @memberof SystemConfigThemeDto
*/
'customCss': string;
}
/**
*
* @export

View File

@ -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

BIN
mobile/openapi/README.md generated

Binary file not shown.

Binary file not shown.

BIN
mobile/openapi/doc/SystemConfigThemeDto.md generated Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -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": {

View File

@ -0,0 +1,6 @@
import { IsString } from 'class-validator';
export class SystemConfigThemeDto {
@IsString()
customCss!: string;
}

View File

@ -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 {

View File

@ -114,6 +114,9 @@ export const defaults = Object.freeze<SystemConfig>({
enabled: true,
days: 30,
},
theme: {
customCss: '',
},
});
export enum FeatureFlag {

View File

@ -115,6 +115,9 @@ const updatedConfig = Object.freeze<SystemConfig>({
enabled: true,
days: 10,
},
theme: {
customCss: '',
},
});
describe(SystemConfigService.name, () => {

View File

@ -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;
};
}

View File

@ -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<string>;
}
/**
*
* @export
* @interface SystemConfigThemeDto
*/
export interface SystemConfigThemeDto {
/**
*
* @type {string}
* @memberof SystemConfigThemeDto
*/
'customCss': string;
}
/**
*
* @export

View File

@ -0,0 +1,53 @@
<script lang="ts">
import { quintOut } from 'svelte/easing';
import { fly } from 'svelte/transition';
export let value: string;
export let label = '';
export let desc = '';
export let required = false;
export let disabled = false;
export let isEdited = false;
const handleInput = (e: Event) => {
value = (e.target as HTMLInputElement).value;
};
</script>
<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}
<div class="text-red-400">*</div>
{/if}
{#if isEdited}
<div
transition:fly={{ x: 10, duration: 200, easing: quintOut }}
class="rounded-full bg-orange-100 px-2 text-[10px] text-orange-900"
>
Unsaved change
</div>
{/if}
</div>
{#if desc}
<p class="immich-form-label pb-2 text-sm" id="{label}-desc">
{desc}
</p>
{:else}
<slot name="desc" />
{/if}
<textarea
class="immich-form-input w-full pb-2"
aria-describedby={desc ? `${label}-desc` : undefined}
aria-labelledby="{label}-label"
id={label}
name={label}
{required}
{value}
on:input={handleInput}
{disabled}
/>
</div>

View File

@ -0,0 +1,98 @@
<script lang="ts">
import {
notificationController,
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import { handleError } from '$lib/utils/handle-error';
import { api, SystemConfigThemeDto } from '@api';
import { isEqual } from 'lodash-es';
import { fade } from 'svelte/transition';
import SettingButtonsRow from '../setting-buttons-row.svelte';
import SettingTextarea from '../setting-textarea.svelte';
export let themeConfig: SystemConfigThemeDto; // this is the config that is being edited
export let disabled = false;
let savedConfig: SystemConfigThemeDto;
let defaultConfig: SystemConfigThemeDto;
async function getConfigs() {
[savedConfig, defaultConfig] = await Promise.all([
api.systemConfigApi.getConfig().then((res) => res.data.theme),
api.systemConfigApi.getDefaults().then((res) => res.data.theme),
]);
}
async function saveSetting() {
try {
const { data: current } = await api.systemConfigApi.getConfig();
const { data: updated } = await api.systemConfigApi.updateConfig({
systemConfigDto: {
...current,
theme: themeConfig,
},
});
themeConfig = { ...updated.theme };
savedConfig = { ...updated.theme };
notificationController.show({ message: 'Theme saved', type: NotificationType.Info });
} catch (error) {
handleError(error, 'Unable to save settings');
}
}
async function reset() {
const { data: resetConfig } = await api.systemConfigApi.getConfig();
themeConfig = { ...resetConfig.theme };
savedConfig = { ...resetConfig.theme };
notificationController.show({
message: 'Reset theme to the recent saved theme',
type: NotificationType.Info,
});
}
async function resetToDefault() {
const { data: configs } = await api.systemConfigApi.getDefaults();
themeConfig = { ...configs.theme };
defaultConfig = { ...configs.theme };
notificationController.show({
message: 'Reset theme to default',
type: NotificationType.Info,
});
}
</script>
<div>
{#await getConfigs() then}
<div in:fade={{ duration: 500 }}>
<form autocomplete="off" on:submit|preventDefault>
<div class="ml-4 mt-4 flex flex-col gap-4">
<div class="ml-4">
<SettingTextarea
{disabled}
label="Custom CSS"
desc="Cascading Style Sheets allow the design of Immich to be customized."
bind:value={themeConfig.customCss}
required={true}
isEdited={themeConfig.customCss !== savedConfig.customCss}
/>
<SettingButtonsRow
on:reset={reset}
on:save={saveSetting}
on:reset-to-default={resetToDefault}
showResetToDefault={!isEqual(savedConfig, defaultConfig)}
{disabled}
/>
</div>
</div>
</form>
</div>
{/await}
</div>

View File

@ -67,6 +67,7 @@
<svelte:head>
<title>{$page.data.meta?.title || 'Web'} - Immich</title>
<link rel="manifest" href="/manifest.json" />
<link rel="stylesheet" href="/custom.css" />
<meta name="theme-color" content="currentColor" />
<FaviconHeader />
<AppleHeader />

View File

@ -10,6 +10,7 @@
import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte';
import ThumbnailSettings from '$lib/components/admin-page/settings/thumbnail/thumbnail-settings.svelte';
import TrashSettings from '$lib/components/admin-page/settings/trash-settings/trash-settings.svelte';
import ThemeSettings from '$lib/components/admin-page/settings/theme/theme-settings.svelte';
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import { downloadManager } from '$lib/stores/download';
@ -96,6 +97,10 @@
/>
</SettingAccordion>
<SettingAccordion title="Theme Settings" subtitle="Manage customization of the Immich web interface">
<ThemeSettings disabled={$featureFlags.configFile} themeConfig={configs.theme} />
</SettingAccordion>
<SettingAccordion title="Thumbnail Settings" subtitle="Manage the resolution of thumbnail sizes">
<ThumbnailSettings disabled={$featureFlags.configFile} thumbnailConfig={configs.thumbnail} />
</SettingAccordion>

View File

@ -0,0 +1,9 @@
import { RequestHandler, text } from '@sveltejs/kit';
export const GET = (async ({ locals: { api } }) => {
const { customCss } = await api.systemConfigApi.getConfig().then((res) => res.data.theme);
return text(customCss, {
headers: {
'Content-Type': 'text/css',
},
});
}) satisfies RequestHandler;