1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-12-23 01:27:14 +02:00

Create theming config #587 #642

This commit is contained in:
Patrik J. Braun 2023-03-27 15:30:38 +02:00
parent 868dd6433e
commit f9bd62db3f
8 changed files with 210 additions and 15 deletions

View File

@ -101,6 +101,8 @@ export class PublicRouter {
.replace(/'/g, ''');
res.tpl.Config = confCopy;
res.tpl.customHTMLHead = Config.Server.customHTMLHead;
const selectedTheme = Config.Gallery.Themes.availableThemes.find(th=>th.name === Config.Gallery.Themes.selectedTheme)?.theme || '';
res.tpl.usedTheme = selectedTheme;
return next();
};

View File

@ -41,7 +41,7 @@ export type TAGS = {
experimental?: boolean, //is it a beta feature
unit?: string, // Unit info to display on UI
uiIcon?: string,
uiType?: 'SearchQuery', // Hint for the UI about the type
uiType?: 'SearchQuery' | 'ThemeSelector' | 'SelectedThemeSettings', // Hint for the UI about the type
uiOptions?: (string | number)[], //Hint for the UI about the recommended options
uiAllowSpaces?: boolean
uiOptional?: boolean; //makes the tag not "required"
@ -502,6 +502,30 @@ export class ClientLightboxConfig {
loopVideos: boolean = false;
}
@SubConfigClass<TAGS>({tags: {client: true}, softReadonly: true})
export class ThemeConfig {
constructor(name?: string, theme?: string) {
this.name = name;
this.theme = theme;
}
@ConfigProperty({
tags: {
name: $localize`Name`,
} as TAGS,
description: $localize`Name of the theme`
})
name: string;
@ConfigProperty({
tags: {
name: $localize`Theme`,
} as TAGS,
description: $localize`Adds these css settings as it is to the end of the body tag of the page.`
})
theme: string;
}
@SubConfigClass<TAGS>({tags: {client: true}, softReadonly: true})
export class ThemesConfig {
@ -524,6 +548,42 @@ export class ThemesConfig {
description: $localize`Sets the default theme mode that is used for the application.`
})
defaultMode: ThemeModes = ThemeModes.light;
@ConfigProperty({
type: 'string',
tags: {
name: $localize`Selected theme`,
uiDisabled: (sb: ThemesConfig) => !sb.enabled,
uiType: 'ThemeSelector'
} as TAGS,
description: $localize`Selected theme to use on the site.`
})
selectedTheme: 'default' | string = 'classic';
@ConfigProperty({
arrayType: ThemeConfig,
tags: {
name: $localize`Selected theme css`, //this is a 'hack' to the UI settings. UI will only show the selected setting's css
uiDisabled: (sb: ThemesConfig) => !sb.enabled,
relevant: (c: ThemesConfig) => c.selectedTheme !== 'default',
uiType: 'SelectedThemeSettings'
} as TAGS,
description: $localize`Adds these css settings as it is to the end of the body tag of the page.`
})
availableThemes: ThemeConfig[] = [
new ThemeConfig(
'classic',
':root nav.navbar {\n' +
'--bs-navbar-color: rgba(255, 255, 255, 0.55);\n' +
'--bs-navbar-hover-color: rgba(255, 255, 255, 0.75);\n' +
'--bs-navbar-disabled-color: rgba(255, 255, 255, 0.25);\n' +
'--bs-navbar-active-color: #fff;\n' +
'--bs-navbar-brand-color: #fff;\n' +
'--bs-navbar-brand-hover-color: #fff;\n' +
'--bs-bg-opacity: 1;\n' +
'background-color: rgba(var(--bs-dark-rgb), var(--bs-bg-opacity)) !important;\n' +
'}'
)];
}
@ -629,7 +689,8 @@ export class ClientGalleryConfig {
tags: {
name: $localize`Themes`,
priority: ConfigPriority.advanced,
},
} as TAGS,
description:$localize`Pigallery2 uses Bootstrap 5.3 (https://getbootstrap.com/docs/5.3) for design (css, layout). In dark mode it sets 'data-bs-theme="dark"' to the <html> to take advantage bootstrap's color modes. For theming, read more at: https://getbootstrap.com/docs/5.3/customize/color-modes/`
})
Themes: ThemesConfig = new ThemesConfig();
}

View File

@ -1,14 +1,13 @@
import {ClientConfig} from './ClientConfig';
import {WebConfigClass} from 'typeconfig/src/decorators/class/WebConfigClass';
import {WebConfigClassBuilder} from 'typeconfig/src/decorators/builders/WebConfigClassBuilder';
import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty';
import {IWebConfigClass} from 'typeconfig/common';
/**
* These configuration will be available at frontend and backend too
*/
@WebConfigClass()
export class ClientClass extends ClientConfig{
export class ClientClass extends ClientConfig {
}
// ConfigInject is getting injected form the server side to the global scope

View File

@ -3,7 +3,7 @@
[class.navbar-keep-top]="navbarKeepTop"
[class.hide-navbar]="!navbarKeepTop && shouldHideNavbar"
[class.animate-navbar]="animateNavbar">
<nav class="navbar navbar-expand navbar-dark bg-dark">
<nav class="navbar navbar-expand">
<div class="container-fluid">
<a class="navbar-brand d-none d-sm-block" [routerLink]="['/gallery']"
[queryParams]="queryService.getParams()">

View File

@ -61,7 +61,7 @@
</ng-template>
<ng-template #saveSearchModal>
<!-- sharing Modal-->
<!-- search Modal-->
<div class="modal-header">
<h5 class="modal-title" i18n>Save search to album</h5>
<button type="button" class="btn-close" (click)="hideSaveSearchModal()" data-dismiss="modal" aria-label="Close">

View File

@ -53,9 +53,9 @@
</select>
<div class="form-check form-switch fs-5">
<div class="form-check form-switch fs-5"
*ngSwitchCase="'Boolean'">
<input class="form-check-input"
*ngSwitchCase="'Boolean'"
[id]="idName"
[name]="idName"
[title]="title"
@ -66,6 +66,82 @@
[(ngModel)]="state.value">
</div>
<div class="input-group"
*ngSwitchCase="'ThemeSelector'">
<select
[id]="idName"
[name]="idName"
[title]="title"
(ngModelChange)="onChange($event)"
[disabled]="state.readonly || Disabled"
class="form-select" [(ngModel)]="state.value">
<option *ngFor="let th of AvailableThemes" [ngValue]="th.key">{{th.value}}
</option>
</select>
<button class="btn btn-primary"
(click)="showNewThemeModal(newThemeModal)">
<span class="oi oi-plus me-2"></span>
<ng-container i18n>Add new</ng-container>
</button>
<button class="btn btn-danger"
*ngIf="state.value !== 'default'"
(click)="removeTheme()">
<span class="oi oi-trash"></span>
</button>
<ng-template #newThemeModal>
<div class="modal-header">
<h5 class="modal-title" i18n>Add new theme</h5>
<button type="button" class="btn-close" (click)="hideNewThemeModal()" data-dismiss="modal"
aria-label="Close">
</button>
</div>
<div class="modal-body">
<form class="form-horizontal">
<label for="themeName" i18n>Theme name</label>
<input
id="themeName"
name="themeName"
placeholder="name"
i18n-placeholder
class="form-control input-md"
required="required"
[(ngModel)]="newThemeName"
type="text"/>
</form>
</div>
<div class="modal-footer">
<div class="btn-group float-end row mt-2" role="group" style="display: block">
<div class="pe-0">
<button class="btn btn-primary" type="button"
(click)="addNewTheme()">
<span class="oi oi-plus me-2"></span>
<ng-container i18n>Add new Theme</ng-container>
</button>
</div>
</div>
</div>
</ng-template>
</div>
<textarea
rows="5"
*ngSwitchCase="'SelectedThemeSettings'"
type="text"
class="form-control"
[title]="title"
[id]="idName"
[(ngModel)]="SelectedThemeSettings.theme"
(ngModelChange)="onChange($event)"
[name]="idName"
[disabled]="state.readonly || Disabled"
required="required">
</textarea>
<app-settings-workflow
class="w-100"
*ngSwitchCase="'JobScheduleConfig'"
@ -231,7 +307,7 @@
</div>
</div>
<div class="row me-0" >
<div class="row me-0">
<div class="col pe-0">
<button class="btn btn-primary float-end"
[disabled]="state.readonly"
@ -285,7 +361,7 @@
triggers="mouseenter:mouseleave"
placement="bottom"
[popover]="popTemplate"
class="oi oi-warning text-warning warning-icon ms-2" ></span>
class="oi oi-warning text-warning warning-icon ms-2"></span>
</div>
</div>

View File

@ -1,13 +1,14 @@
import {Component, forwardRef, OnChanges} from '@angular/core';
import {Component, forwardRef, OnChanges, TemplateRef} from '@angular/core';
import {ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator,} from '@angular/forms';
import {Utils} from '../../../../../../common/Utils';
import {propertyTypes} from 'typeconfig/common';
import {SearchQueryParserService} from '../../../gallery/search/search-query-parser.service';
import {MapLayers, NavigationLinkConfig, NavigationLinkTypes, TAGS} from '../../../../../../common/config/public/ClientConfig';
import {MapLayers, NavigationLinkConfig, NavigationLinkTypes, TAGS, ThemeConfig} from '../../../../../../common/config/public/ClientConfig';
import {SettingsService} from '../../settings.service';
import {WebConfig} from '../../../../../../common/config/private/WebConfig';
import {JobScheduleConfig, UserConfig} from '../../../../../../common/config/private/PrivateConfig';
import {enumToTranslatedArray} from '../../../EnumTranslations';
import {BsModalService} from '../../../../../../../node_modules/ngx-bootstrap/modal';
interface IState {
shouldHide(): boolean;
@ -67,10 +68,12 @@ export class SettingsEntryComponent
public type: string | object;
public arrayType: string;
public uiType: string;
newThemeModalRef: any;
constructor(private searchQueryParserService: SearchQueryParserService,
public settingsService: SettingsService,
private modalService: BsModalService,
) {
}
@ -171,6 +174,18 @@ export class SettingsEntryComponent
}
get AvailableThemes() {
return [{
key: 'default',
value: $localize`default`
}, ...(this.state.rootConfig as any).__state.availableThemes.value
.map((th: ThemeConfig) => ({key: th.name, value: th.name}))];
}
get SelectedThemeSettings(): { theme: string } {
return (this.state.value as ThemeConfig[]).find(th => th.name === (this.state.rootConfig as any).__state.selectedTheme.value) || {theme: 'N/A'};
}
get Disabled() {
if (!this.state?.tags?.uiDisabled) {
@ -209,8 +224,8 @@ export class SettingsEntryComponent
this.arrayType !== 'UserConfig') {
this.uiType = 'StringInput';
}
if (this.type === 'SearchQuery') {
this.uiType = 'SearchQuery';
if (this.type === this.state.tags?.uiType) {
this.uiType = this.state.tags?.uiType;
} else if (this.state.isEnumType) {
this.uiType = 'EnumType';
} else if (this.type === 'boolean') {
@ -301,6 +316,7 @@ export class SettingsEntryComponent
public onTouched = (): void => {
// empty
};
newThemeName: string;
public writeValue(obj: IState): void {
this.state = obj;
@ -324,7 +340,6 @@ export class SettingsEntryComponent
}
this.state.value.push(this.state.value[this.state.value.length - 1]);
}
console.dir(this.state.value);
}
remove(i: number): void {
@ -350,6 +365,44 @@ export class SettingsEntryComponent
}
addNewTheme(): void {
const availableThemes = (this.state.rootConfig as any).__state.availableThemes;
if (!this.newThemeName ||
(availableThemes.value as ThemeConfig[]).find(th => th.name === this.newThemeName)) {
return;
}
this.state.value = this.newThemeName;
availableThemes.value.push(new availableThemes.arrayType(this.newThemeName, ''));
this.newThemeName = '';
this.onChange(null);
this.hideNewThemeModal();
}
removeTheme(): void {
const availableThemes = (this.state.rootConfig as any).__state.availableThemes;
const i = (availableThemes.value as ThemeConfig[]).findIndex(th => th.name === this.state.value);
if (i >= 0) {
this.state.value = 'default';
availableThemes.value.splice(i, 1);
this.onChange(null);
}
}
showNewThemeModal(template: TemplateRef<any>): void {
this.newThemeModalRef = this.modalService.show(template, {
class: 'modal-lg',
});
document.body.style.paddingRight = '0px';
}
public hideNewThemeModal(): void {
this.newThemeModalRef.hide();
this.newThemeModalRef = null;
}
}

View File

@ -37,5 +37,9 @@
<h2>Loading...</h2>
</div>
</app-pi-gallery2>
<style>
<%- usedTheme %>
</style>
</body>
</html>