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

Creating svg picker for svg icon config #587

#667
This commit is contained in:
Patrik J. Braun 2023-07-08 16:55:16 +02:00
parent 4a1daff947
commit 6c0c883298
5 changed files with 155 additions and 35 deletions

View File

@ -41,7 +41,7 @@ export type TAGS = {
experimental?: boolean, //is it a beta feature experimental?: boolean, //is it a beta feature
unit?: string, // Unit info to display on UI unit?: string, // Unit info to display on UI
uiIcon?: string, uiIcon?: string,
uiType?: 'SearchQuery' | 'ThemeSelector' | 'SelectedThemeSettings', // Hint for the UI about the type uiType?: 'SearchQuery' | 'ThemeSelector' | 'SelectedThemeSettings' | 'SVGIconConfig', // Hint for the UI about the type
uiOptions?: (string | number)[], //Hint for the UI about the recommended options uiOptions?: (string | number)[], //Hint for the UI about the recommended options
uiAllowSpaces?: boolean uiAllowSpaces?: boolean
uiOptional?: boolean; //makes the tag not "required" uiOptional?: boolean; //makes the tag not "required"
@ -318,27 +318,23 @@ export class MapLayers {
darkLayer: boolean = false; darkLayer: boolean = false;
} }
export enum MapPathGroupTypes {
Transportation = 1, Sport, Custom
}
@SubConfigClass({tags: {client: true}, softReadonly: true}) @SubConfigClass({tags: {client: true}, softReadonly: true})
export class SVGIconConfig { export class SVGIconConfig {
constructor(viewBoxWidth: number = 512, path: string = '') { constructor(viewBox: string = '0 0 512 512', path: string = '') {
this.viewBoxWidth = viewBoxWidth; this.viewBox = viewBox;
this.path = path; this.path = path;
} }
@ConfigProperty({ @ConfigProperty({
tags: { tags: {
name: $localize`SBG icon viewBox with`, name: $localize`SVG icon viewBox`,
priority: ConfigPriority.advanced priority: ConfigPriority.advanced
}, },
description: $localize`You need the with from the SVG viewBox (assuming height is 512). See: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox`, description: $localize`SVG path viewBox. See: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox`,
}) })
viewBoxWidth: number = 512; viewBox: string = '0 0 512 512';
@ConfigProperty({ @ConfigProperty({
tags: { tags: {
@ -372,7 +368,8 @@ export class PathThemeConfig {
@ConfigProperty({ @ConfigProperty({
tags: { tags: {
name: $localize`Dash pattern`, name: $localize`Dash pattern`,
priority: ConfigPriority.advanced priority: ConfigPriority.advanced,
uiOptions: ['', '4', '4 1', '4 8', '4 1 2', '0 4 0', '4 1 2 3'],
}, },
description: $localize`Dash pattern of the path. Represents the spacing and length of the dash. Read more about dash array at: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray.`, description: $localize`Dash pattern of the path. Represents the spacing and length of the dash. Read more about dash array at: https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray.`,
}) })
@ -383,8 +380,9 @@ export class PathThemeConfig {
type: SVGIconConfig, type: SVGIconConfig,
tags: { tags: {
name: $localize`Svg Icon`, name: $localize`Svg Icon`,
uiType: 'SVGIconConfig',
priority: ConfigPriority.advanced priority: ConfigPriority.advanced
}, } as TAGS,
description: $localize`Set the icon of the map marker pin.`, description: $localize`Set the icon of the map marker pin.`,
}) })
svgIcon: SVGIconConfig = new SVGIconConfig(); svgIcon: SVGIconConfig = new SVGIconConfig();
@ -406,7 +404,7 @@ export class MapPathGroupThemeConfig {
name: $localize`Matchers`, name: $localize`Matchers`,
priority: ConfigPriority.advanced priority: ConfigPriority.advanced
}, },
description: $localize`List of regex string to match the name of the path. Case insensitive.`, description: $localize`List of regex string to match the name of the path. Case insensitive. Empty list matches everything.`,
}) })
matchers: string[] = []; matchers: string[] = [];
@ -527,7 +525,7 @@ export class ClientMapConfig {
['flight', 'flying', 'drive', 'driving'], ['flight', 'flying', 'drive', 'driving'],
new PathThemeConfig('var(--bs-orange)', new PathThemeConfig('var(--bs-orange)',
'4 8', '4 8',
new SVGIconConfig(567, 'M482.3 192c34.2 0 93.7 29 93.7 64c0 36-59.5 64-93.7 64l-116.6 0L265.2 495.9c-5.7 10-16.3 16.1-27.8 16.1l-56.2 0c-10.6 0-18.3-10.2-15.4-20.4l49-171.6L112 320 68.8 377.6c-3 4-7.8 6.4-12.8 6.4l-42 0c-7.8 0-14-6.3-14-14c0-1.3 .2-2.6 .5-3.9L32 256 .5 145.9c-.4-1.3-.5-2.6-.5-3.9c0-7.8 6.3-14 14-14l42 0c5 0 9.8 2.4 12.8 6.4L112 192l102.9 0-49-171.6C162.9 10.2 170.6 0 181.2 0l56.2 0c11.5 0 22.1 6.2 27.8 16.1L365.7 192l116.6 0z') new SVGIconConfig('0 0 567 512', 'M482.3 192c34.2 0 93.7 29 93.7 64c0 36-59.5 64-93.7 64l-116.6 0L265.2 495.9c-5.7 10-16.3 16.1-27.8 16.1l-56.2 0c-10.6 0-18.3-10.2-15.4-20.4l49-171.6L112 320 68.8 377.6c-3 4-7.8 6.4-12.8 6.4l-42 0c-7.8 0-14-6.3-14-14c0-1.3 .2-2.6 .5-3.9L32 256 .5 145.9c-.4-1.3-.5-2.6-.5-3.9c0-7.8 6.3-14 14-14l42 0c5 0 9.8 2.4 12.8 6.4L112 192l102.9 0-49-171.6C162.9 10.2 170.6 0 181.2 0l56.2 0c11.5 0 22.1 6.2 27.8 16.1L365.7 192l116.6 0z')
) )
)]), )]),
new MapPathGroupConfig('Sport', new MapPathGroupConfig('Sport',
@ -535,7 +533,7 @@ export class ClientMapConfig {
['run', 'walk', 'hike', 'hiking', 'bike', 'biking', 'cycling', 'skiing'], ['run', 'walk', 'hike', 'hiking', 'bike', 'biking', 'cycling', 'skiing'],
new PathThemeConfig('var(--bs-primary)', new PathThemeConfig('var(--bs-primary)',
'', '',
new SVGIconConfig(417, 'M320 48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM125.7 175.5c9.9-9.9 23.4-15.5 37.5-15.5c1.9 0 3.8 .1 5.6 .3L137.6 254c-9.3 28 1.7 58.8 26.8 74.5l86.2 53.9-25.4 88.8c-4.9 17 5 34.7 22 39.6s34.7-5 39.6-22l28.7-100.4c5.9-20.6-2.6-42.6-20.7-53.9L238 299l30.9-82.4 5.1 12.3C289 264.7 323.9 288 362.7 288H384c17.7 0 32-14.3 32-32s-14.3-32-32-32H362.7c-12.9 0-24.6-7.8-29.5-19.7l-6.3-15c-14.6-35.1-44.1-61.9-80.5-73.1l-48.7-15c-11.1-3.4-22.7-5.2-34.4-5.2c-31 0-60.8 12.3-82.7 34.3L57.4 153.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l23.1-23.1zM91.2 352H32c-17.7 0-32 14.3-32 32s14.3 32 32 32h69.6c19 0 36.2-11.2 43.9-28.5L157 361.6l-9.5-6c-17.5-10.9-30.5-26.8-37.9-44.9L91.2 352z') new SVGIconConfig('0 0 417 512', 'M320 48a48 48 0 1 0 -96 0 48 48 0 1 0 96 0zM125.7 175.5c9.9-9.9 23.4-15.5 37.5-15.5c1.9 0 3.8 .1 5.6 .3L137.6 254c-9.3 28 1.7 58.8 26.8 74.5l86.2 53.9-25.4 88.8c-4.9 17 5 34.7 22 39.6s34.7-5 39.6-22l28.7-100.4c5.9-20.6-2.6-42.6-20.7-53.9L238 299l30.9-82.4 5.1 12.3C289 264.7 323.9 288 362.7 288H384c17.7 0 32-14.3 32-32s-14.3-32-32-32H362.7c-12.9 0-24.6-7.8-29.5-19.7l-6.3-15c-14.6-35.1-44.1-61.9-80.5-73.1l-48.7-15c-11.1-3.4-22.7-5.2-34.4-5.2c-31 0-60.8 12.3-82.7 34.3L57.4 153.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l23.1-23.1zM91.2 352H32c-17.7 0-32 14.3-32 32s14.3 32 32 32h69.6c19 0 36.2-11.2 43.9-28.5L157 361.6l-9.5-6c-17.5-10.9-30.5-26.8-37.9-44.9L91.2 352z')
) )
)]), )]),
new MapPathGroupConfig('Other paths', new MapPathGroupConfig('Other paths',

View File

@ -3,7 +3,7 @@ import {DivIcon, setOptions} from 'leaflet';
export interface SvgIconOptions { export interface SvgIconOptions {
color?: string; color?: string;
svgPath?: string; svgPath?: string;
width?: number; viewBox?: string;
small?: boolean; small?: boolean;
} }
@ -11,8 +11,8 @@ const SvgIcon: { new(options?: SvgIconOptions): DivIcon } = DivIcon.extend({
initialize: function(options: SvgIconOptions = {}) { initialize: function(options: SvgIconOptions = {}) {
options.color = options.color || 'var(--bs-primary)'; options.color = options.color || 'var(--bs-primary)';
options.svgPath = options.svgPath || 'M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512z'; options.svgPath = options.svgPath || 'M256 512c141.4 0 256-114.6 256-256S397.4 0 256 0S0 114.6 0 256S114.6 512 256 512z';
options.width = options.width || 512; options.viewBox = options.viewBox || '0 0 512 512';
const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ' + options.width + ' 512"><path fill="' + options.color + '" d="' + options.svgPath + '"/></svg>'; const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="' + options.viewBox + '"><path fill="' + options.color + '" d="' + options.svgPath + '"/></svg>';
setOptions(this, { setOptions(this, {
iconSize: options.small ? [15, 15] : [30, 30], iconSize: options.small ? [15, 15] : [30, 30],
iconAnchor: options.small ? [15, 28] : [15, 35], iconAnchor: options.small ? [15, 28] : [15, 35],

View File

@ -189,7 +189,7 @@ export class GalleryMapLightboxComponent implements OnChanges, OnDestroy {
icon: MarkerFactory.getSvgIcon({ icon: MarkerFactory.getSvgIcon({
color: ths.theme.color, color: ths.theme.color,
svgPath: ths.theme.svgIcon?.path, svgPath: ths.theme.svgIcon?.path,
width: ths.theme.svgIcon?.viewBoxWidth viewBox: ths.theme.svgIcon?.viewBox
}) })
}; };
}) })

View File

@ -151,6 +151,79 @@
(ngModelChange)="onChange($event)"> (ngModelChange)="onChange($event)">
</app-settings-workflow> </app-settings-workflow>
<ng-container *ngSwitchCase="'SVGIconConfig'">
<button class="btn btn-outline-primary"
(click)="showIconModal(iconModalTmp)">
<svg xmlns="http://www.w3.org/2000/svg"
width="1em"
[attr.viewBox]="state.value.viewBox || '0 0 512 512'">
<path [attr.d]="state.value.path"/>
</svg>
</button>
<ng-template #iconModalTmp>
<div class="modal-header">
<h5 class="modal-title" i18n>Icon</h5>
<button type="button" class="btn-close" (click)="hideIconModal()" data-dismiss="modal"
aria-label="Close">
</button>
</div>
<div class="modal-body">
<div class="row mb-4">
<div class="col text-center">
<svg xmlns="http://www.w3.org/2000/svg"
width="2em"
[attr.viewBox]="state.value.viewBox || '0 0 512 512'">
<path [attr.d]="state.value.path"/>
</svg>
</div>
</div>
<div *ngIf="iconModal?.error" class="alert alert-danger">{{iconModal.error}}</div>
<div class="row">
<div class="col-md-2 control-label">
<label [for]="'icon_f_'+idName" class="form-label" i18n>Load from SVG file</label>
</div>
<div class="col-md-10">
<input type="file" class="form-control" [id]="'icon_f_'+idName" accept="image/svg+xml"
(change)="newSvgFile($event)">
<small class="form-text text-muted" i18n>To auto load these values from file: pick an SVG file with a
single 'path'. You can use e.g: http://fontawesome.com/icons.
</small>
</div>
</div>
<hr/>
<app-settings-entry
[ngModel]="state.value.__state.viewBox"
[name]="'icon_w_'+idName"
[id]="'icon_w_'+idName"
(change)="onChange($event)"></app-settings-entry>
<app-settings-entry
[ngModel]="state.value.__state.path"
[name]="'icon_p_'+idName"
[id]="'icon_p_'+idName"
(change)="onChange($event)"></app-settings-entry>
</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)="hideIconModal()">
<ng-container i18n>Save & Close</ng-container>
</button>
</div>
</div>
</div>
</ng-template>
</ng-container>
<ng-container *ngSwitchCase="'MapLayers'"> <ng-container *ngSwitchCase="'MapLayers'">
<div class="container"> <div class="container">
<table class="table"> <table class="table">
@ -227,17 +300,11 @@
</div> </div>
<div class="row mt-1 mb-1 bg-body-tertiary"> <div class="row mt-1 mb-1 bg-body-tertiary">
<app-settings-entry <app-settings-entry
[ngModel]="val.theme.svgIcon.__state.viewBoxWidth" [ngModel]="val.theme.__state.svgIcon"
[name]="'viewBoxWidth_n_'+idName+i" [name]="'svgIcon_'+idName+i"
[id]="'viewBoxWidth_n_'+idName+i" [id]="'svgIcon_'+idName+i"
(change)="onChange($event)"></app-settings-entry> (ngModelChange)="onChange($event)">
</div> </app-settings-entry>
<div class="row mt-1 mb-1 bg-body-tertiary">
<app-settings-entry
[ngModel]="val.theme.svgIcon.__state.path"
[name]="'path_n_'+idName+i"
[id]="'path_n_'+idName+i"
(change)="onChange($event)"></app-settings-entry>
</div> </div>
</div> </div>
@ -279,7 +346,8 @@
[ngModel]="arr.__state.matchers" [ngModel]="arr.__state.matchers"
[name]="'arr_m_'+idName+i" [name]="'arr_m_'+idName+i"
[id]="'arr_m_'+idName+i" [id]="'arr_m_'+idName+i"
(change)="onChange($event)"></app-settings-entry> (ngModelChange)="onChange($event)">
</app-settings-entry>
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,7 +3,15 @@ import {ControlValueAccessor, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors
import {Utils} from '../../../../../../common/Utils'; import {Utils} from '../../../../../../common/Utils';
import {propertyTypes} from 'typeconfig/common'; import {propertyTypes} from 'typeconfig/common';
import {SearchQueryParserService} from '../../../gallery/search/search-query-parser.service'; import {SearchQueryParserService} from '../../../gallery/search/search-query-parser.service';
import {MapLayers, MapPathGroupConfig, MapPathGroupThemeConfig, NavigationLinkConfig, NavigationLinkTypes, TAGS, ThemeConfig} from '../../../../../../common/config/public/ClientConfig'; import {
MapLayers,
MapPathGroupConfig,
MapPathGroupThemeConfig,
NavigationLinkConfig,
NavigationLinkTypes,
TAGS,
ThemeConfig
} from '../../../../../../common/config/public/ClientConfig';
import {SettingsService} from '../../settings.service'; import {SettingsService} from '../../settings.service';
import {WebConfig} from '../../../../../../common/config/private/WebConfig'; import {WebConfig} from '../../../../../../common/config/private/WebConfig';
import {JobScheduleConfig, UserConfig} from '../../../../../../common/config/private/PrivateConfig'; import {JobScheduleConfig, UserConfig} from '../../../../../../common/config/private/PrivateConfig';
@ -69,6 +77,7 @@ export class SettingsEntryComponent
public arrayType: string; public arrayType: string;
public uiType: string; public uiType: string;
newThemeModalRef: any; newThemeModalRef: any;
iconModal: { ref?: any, error?: string };
constructor(private searchQueryParserService: SearchQueryParserService, constructor(private searchQueryParserService: SearchQueryParserService,
@ -217,11 +226,16 @@ export class SettingsEntryComponent
} else { } else {
this.arrayType = this.state.arrayType; this.arrayType = this.state.arrayType;
} }
if (this.state.tags?.uiOptions) {
this.state.isEnumType = true;
}
this.uiType = this.arrayType; this.uiType = this.arrayType;
if (!this.state.isEnumType && if (!this.state.isEnumType &&
!this.state.isEnumArrayType && !this.state.isEnumArrayType &&
this.type !== 'boolean' && this.type !== 'boolean' &&
this.type !== 'SearchQuery' && this.type !== 'SearchQuery' &&
this.type !== 'SVGIconConfig' &&
this.arrayType !== 'MapLayers' && this.arrayType !== 'MapLayers' &&
this.arrayType !== 'NavigationLinkConfig' && this.arrayType !== 'NavigationLinkConfig' &&
this.arrayType !== 'MapPathGroupConfig' && this.arrayType !== 'MapPathGroupConfig' &&
@ -242,9 +256,6 @@ export class SettingsEntryComponent
this.placeholder = this.state.tags?.hint || this.state.default; this.placeholder = this.state.tags?.hint || this.state.default;
if (this.state.tags?.uiOptions) {
this.state.isEnumType = true;
}
this.title = ''; this.title = '';
if (this.state.readonly) { if (this.state.readonly) {
this.title = $localize`readonly` + ', '; this.title = $localize`readonly` + ', ';
@ -404,11 +415,54 @@ export class SettingsEntryComponent
} }
showIconModal(template: TemplateRef<any>): void {
this.iconModal = {};
this.iconModal.ref = this.modalService.show(template, {
class: 'modal-lg',
});
document.body.style.paddingRight = '0px';
}
public hideNewThemeModal(): void { public hideNewThemeModal(): void {
this.newThemeModalRef.hide(); this.newThemeModalRef.hide();
this.newThemeModalRef = null; this.newThemeModalRef = null;
} }
hideIconModal(): void {
if (!this.iconModal) {
return;
}
this.iconModal.ref.hide();
delete this.iconModal;
}
newSvgFile(event: Event): void {
const file: File = (event.target as HTMLInputElement).files[0];
const reader = new FileReader();
reader.onload = () => {
console.log(reader.result);
const parser = new DOMParser();
const doc = parser.parseFromString(reader.result as string, 'image/svg+xml');
try {
const wb = doc.documentElement.getAttribute('viewBox');
const path = doc.documentElement.getElementsByTagName('path')[0].getAttribute('d');
this.state.value.path = path;
this.state.value.viewBox = wb;
} catch (e) {
console.error(e);
if (this.iconModal) {
this.iconModal.error = 'Can\'t parse SVG file: ' + e.toState;
}
}
this.onChange(null);
};
reader.readAsText(file);
}
} }