diff --git a/package-lock.json b/package-lock.json index d018a892..86ec1406 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "pigallery2", - "version": "1.8.0", + "version": "1.8.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -18721,9 +18721,9 @@ } }, "typeconfig": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/typeconfig/-/typeconfig-2.0.0.tgz", - "integrity": "sha512-IwZH+P8J4qhyrOfKzJZJx6raEkaBjjZIiE+rzrWgdOhqVhO5Cv+pkOQdNo+z/Wq5wER5YWeDNX7Wdbqv0jt6IA==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/typeconfig/-/typeconfig-2.0.6.tgz", + "integrity": "sha512-OnXPXSDaK1mzH6dJ1HB9Q70ruJYngEhemwL9Y8+nG5E40Je4MMODuEY+tfjMtIiFIN772DU5Q+NebulLZNDjpw==", "requires": { "optimist": "0.6.1" } diff --git a/package.json b/package.json index 31f85da9..5c635a95 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pigallery2", - "version": "1.8.0", + "version": "1.8.1", "description": "This is a photo gallery optimised for running low resource servers (especially on raspberry pi)", "author": "Patrik J. Braun", "homepage": "https://github.com/bpatrik/PiGallery2", @@ -16,6 +16,7 @@ "coverage": "nyc report --reporter=text-lcov | coveralls", "start": "node ./src/backend/index", "run-dev": "ng build --aot --watch --output-path=./dist --i18n-locale en --i18n-file src/frontend/translate/messages.en.xlf --i18n-missing-translation warning", + "run-dev-hu": "ng build --aot --watch --output-path=./dist --i18n-locale hu --i18n-file src/frontend/translate/messages.hu.xlf --i18n-missing-translation warning", "build-stats": "ng build --aot --prod --stats-json --output-path=./dist --i18n-locale en --i18n-file src/frontend/translate/messages.en.xlf --i18n-missing-translation warning", "merge-new-translation": "gulp merge-new-translation", "add-translation": "gulp add-translation", @@ -47,7 +48,7 @@ "sqlite3": "4.1.1", "ts-exif-parser": "0.1.4", "ts-node-iptc": "1.0.11", - "typeconfig": "2.0.0", + "typeconfig": "2.0.6", "typeorm": "0.2.21", "winston": "2.4.4" }, diff --git a/src/backend/middlewares/RenderingMWs.ts b/src/backend/middlewares/RenderingMWs.ts index cbe1718f..9ae70f68 100644 --- a/src/backend/middlewares/RenderingMWs.ts +++ b/src/backend/middlewares/RenderingMWs.ts @@ -63,7 +63,10 @@ export class RenderingMWs { public static async renderConfig(req: Request, res: Response, next: NextFunction) { const originalConf = await Config.original(); originalConf.Server.sessionSecret = null; - const message = new Message(null, originalConf.toJSON({attachDefaults: true})); + const message = new Message(null, originalConf.toJSON({ + attachState: true, + attachVolatile: true + })); res.json(message); } diff --git a/src/backend/routes/PublicRouter.ts b/src/backend/routes/PublicRouter.ts index f889a36d..ea54e575 100644 --- a/src/backend/routes/PublicRouter.ts +++ b/src/backend/routes/PublicRouter.ts @@ -82,7 +82,7 @@ export class PublicRouter { res.tpl.user.csrfToken = req.csrfToken(); } } - res.tpl.clientConfig = {Client: Config.Client}; + res.tpl.clientConfig = {Client: Config.Client.toJSON({attachVolatile: true})}; return next(); }); diff --git a/src/common/Utils.ts b/src/common/Utils.ts index 3106bbe7..c77238a9 100644 --- a/src/common/Utils.ts +++ b/src/common/Utils.ts @@ -61,6 +61,10 @@ export class Utils { if (!object) { return false; } + + if (Array.isArray(object) && object.length !== filter.length) { + return false; + } const keys = Object.keys(filter); for (let i = 0; i < keys.length; i++) { const key = keys[i]; @@ -158,10 +162,7 @@ export class Utils { }); } - public static enumToArray(EnumType: any): Array<{ - key: number; - value: string; - }> { + public static enumToArray(EnumType: any): { key: number; value: string }[] { const arr: Array<{ key: number; value: string; }> = []; for (const enumMember in EnumType) { if (!EnumType.hasOwnProperty(enumMember)) { diff --git a/src/common/config/private/Config.ts b/src/common/config/private/Config.ts index ea493f60..1dd5632b 100644 --- a/src/common/config/private/Config.ts +++ b/src/common/config/private/Config.ts @@ -2,10 +2,8 @@ import {IPrivateConfig, ServerConfig} from './PrivateConfig'; import {ClientConfig} from '../public/ClientConfig'; import * as crypto from 'crypto'; import * as path from 'path'; -import {ConfigClass} from 'typeconfig/src/decorators/class/ConfigClass'; -import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty'; -import {IConfigClass} from 'typeconfig/src/decorators/class/IConfigClass'; -import {ConfigClassBuilder} from 'typeconfig/src/decorators/builders/ConfigClassBuilder'; +import {ConfigClass, ConfigClassBuilder} from 'typeconfig/node'; +import {ConfigProperty, IConfigClass} from 'typeconfig/common'; @ConfigClass({ @@ -16,7 +14,7 @@ import {ConfigClassBuilder} from 'typeconfig/src/decorators/builders/ConfigClass cli: { enable: { configPath: true, - attachDefaults: true, + attachState: true, attachDescription: true, rewriteCLIConfig: true, rewriteENVConfig: true, @@ -33,7 +31,7 @@ export class PrivateConfigClass implements IPrivateConfig { @ConfigProperty() Server: ServerConfig.Config = new ServerConfig.Config(); @ConfigProperty() - Client: ClientConfig.Config = new ClientConfig.Config(); + Client: IConfigClass & ClientConfig.Config = (new ClientConfig.Config()); constructor() { if (!this.Server.sessionSecret || this.Server.sessionSecret.length === 0) { diff --git a/src/common/config/private/PrivateConfig.ts b/src/common/config/private/PrivateConfig.ts index 73c65f9c..9c899a37 100644 --- a/src/common/config/private/PrivateConfig.ts +++ b/src/common/config/private/PrivateConfig.ts @@ -3,8 +3,8 @@ import 'reflect-metadata'; import {DefaultsJobs} from '../../entities/job/JobDTO'; import {JobScheduleDTO, JobTrigger, JobTriggerType} from '../../entities/job/JobScheduleDTO'; import {ClientConfig} from '../public/ClientConfig'; -import { SubConfigClass } from 'typeconfig/src/decorators/class/SubConfigClass'; -import { ConfigProperty } from 'typeconfig/src/decorators/property/ConfigPropoerty'; +import {SubConfigClass} from 'typeconfig/src/decorators/class/SubConfigClass'; +import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty'; export module ServerConfig { export enum DatabaseType { @@ -37,14 +37,14 @@ export module ServerConfig { @SubConfigClass() export class MySQLConfig { @ConfigProperty({envAlias: 'MYSQL_HOST'}) - host: string = ''; - @ConfigProperty({envAlias: 'MYSQL_PORT'}) + host: string = 'localhost'; + @ConfigProperty({envAlias: 'MYSQL_PORT', min: 0, max: 65535}) port: number = 3306; @ConfigProperty({envAlias: 'MYSQL_DATABASE'}) - database: string = ''; + database: string = 'pigallery2'; @ConfigProperty({envAlias: 'MYSQL_USERNAME'}) username: string = ''; - @ConfigProperty({envAlias: 'MYSQL_PASSWORD'}) + @ConfigProperty({envAlias: 'MYSQL_PASSWORD', type: 'password'}) password: string = ''; } @@ -93,13 +93,13 @@ export module ServerConfig { @ConfigProperty({type: ReIndexingSensitivity}) reIndexingSensitivity: ReIndexingSensitivity = ReIndexingSensitivity.low; @ConfigProperty({ - arrayType: String, + arrayType: 'string', description: 'If an entry starts with \'/\' it is treated as an absolute path.' + ' If it doesn\'t start with \'/\' but contains a \'/\', the path is relative to the image directory.' + ' If it doesn\'t contain a \'/\', any folder with this name will be excluded.' }) excludeFolderList: string[] = []; - @ConfigProperty({arrayType: String, description: 'Any folder that contains a file with this name will be excluded from indexing.'}) + @ConfigProperty({arrayType: 'string', description: 'Any folder that contains a file with this name will be excluded from indexing.'}) excludeFileList: string[] = []; } @@ -291,9 +291,9 @@ export module ServerConfig { @SubConfigClass() export class Config { - @ConfigProperty({arrayType: String}) + @ConfigProperty({arrayType: 'string'}) sessionSecret: string[] = []; - @ConfigProperty({type: 'unsignedInt', envAlias: 'PORT'}) + @ConfigProperty({type: 'unsignedInt', envAlias: 'PORT', min: 0, max: 65535}) port: number = 80; @ConfigProperty() host: string = '0.0.0.0'; @@ -305,7 +305,7 @@ export module ServerConfig { Database: DataBaseConfig = new DataBaseConfig(); @ConfigProperty() Sharing: SharingConfig = new SharingConfig(); - @ConfigProperty({description: 'unit: ms'}) + @ConfigProperty({type: 'unsignedInt', description: 'unit: ms'}) sessionTimeout: number = 1000 * 60 * 60 * 24 * 7; // in ms @ConfigProperty() Indexing: IndexingConfig = new IndexingConfig(); diff --git a/src/common/config/private/WebConfig.ts b/src/common/config/private/WebConfig.ts index ac19b853..75ee7408 100644 --- a/src/common/config/private/WebConfig.ts +++ b/src/common/config/private/WebConfig.ts @@ -2,15 +2,14 @@ import 'reflect-metadata'; import {ClientConfig} from '../public/ClientConfig'; import {ServerConfig} from './PrivateConfig'; -import {WebConfigClass} from 'typeconfig/src/decorators/class/WebConfigClass'; -import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty'; -import {ConfigDefaults} from 'typeconfig/src/decorators/property/ConfigDefaults'; +import {WebConfigClass} from 'typeconfig/web'; +import {ConfigProperty, ConfigState} from 'typeconfig/common'; @WebConfigClass() export class WebConfig { - @ConfigDefaults() - Defaults: WebConfig; + @ConfigState() + State: any; @ConfigProperty() Server: ServerConfig.Config = new ServerConfig.Config(); diff --git a/src/common/config/public/ClientConfig.ts b/src/common/config/public/ClientConfig.ts index c51d4d75..13a9d4a1 100644 --- a/src/common/config/public/ClientConfig.ts +++ b/src/common/config/public/ClientConfig.ts @@ -2,23 +2,23 @@ import 'reflect-metadata'; import {SortingMethods} from '../../entities/SortingMethods'; import {UserRoles} from '../../entities/UserDTO'; -import { SubConfigClass } from 'typeconfig/src/decorators/class/SubConfigClass'; -import { ConfigProperty } from 'typeconfig/src/decorators/property/ConfigPropoerty'; +import {SubConfigClass} from 'typeconfig/src/decorators/class/SubConfigClass'; +import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty'; export module ClientConfig { export enum MapProviders { - OpenStreetMap = 0, Mapbox = 1, Custom = 2 + OpenStreetMap = 1, Mapbox = 2, Custom = 3 } @SubConfigClass() export class AutoCompleteConfig { @ConfigProperty() - enabled = true; - @ConfigProperty() - maxItemsPerCategory = 5; - @ConfigProperty() + enabled: boolean = true; + @ConfigProperty({type: 'unsignedInt'}) + maxItemsPerCategory: number = 5; + @ConfigProperty({type: 'unsignedInt'}) cacheTimeout: number = 1000 * 60 * 60; } @@ -28,11 +28,11 @@ export module ClientConfig { enabled: boolean = true; @ConfigProperty() instantSearchEnabled: boolean = true; - @ConfigProperty() + @ConfigProperty({type: 'unsignedInt'}) InstantSearchTimeout: number = 3000; - @ConfigProperty() + @ConfigProperty({type: 'unsignedInt'}) instantSearchCacheTimeout: number = 1000 * 60 * 60; - @ConfigProperty() + @ConfigProperty({type: 'unsignedInt'}) searchCacheTimeout: number = 1000 * 60 * 60; @ConfigProperty() AutoComplete: AutoCompleteConfig = new AutoCompleteConfig(); @@ -66,7 +66,7 @@ export module ClientConfig { enabled: boolean = true; @ConfigProperty() useImageMarkers: boolean = true; - @ConfigProperty() + @ConfigProperty({type: MapProviders}) mapProvider: MapProviders = MapProviders.OpenStreetMap; @ConfigProperty() mapboxAccessToken: string = ''; @@ -76,11 +76,11 @@ export module ClientConfig { @SubConfigClass() export class ThumbnailConfig { - @ConfigProperty() + @ConfigProperty({type: 'unsignedInt', max: 100}) iconSize: number = 45; - @ConfigProperty() + @ConfigProperty({type: 'unsignedInt'}) personThumbnailSize: number = 200; - @ConfigProperty({arrayType: Number}) + @ConfigProperty({arrayType: 'unsignedInt'}) thumbnailSizes: number[] = [240, 480]; @ConfigProperty({volatile: true}) concurrentThumbnailGenerations: number = 1; @@ -184,7 +184,7 @@ export module ClientConfig { authenticationRequired: boolean = true; @ConfigProperty({type: UserRoles}) unAuthenticatedUserRole: UserRoles = UserRoles.Admin; - @ConfigProperty({arrayType: String, volatile: true}) + @ConfigProperty({arrayType: 'string', volatile: true}) languages: string[]; @ConfigProperty() Media: MediaConfig = new MediaConfig(); diff --git a/src/frontend/app/app.module.ts b/src/frontend/app/app.module.ts index 64574a06..28d42ba3 100644 --- a/src/frontend/app/app.module.ts +++ b/src/frontend/app/app.module.ts @@ -92,6 +92,7 @@ import {BackendtextService} from './model/backendtext.service'; import {JobButtonComponent} from './ui/settings/jobs/button/job-button.settings.component'; import {ErrorInterceptor} from './model/network/helper/error.interceptor'; import {CSRFInterceptor} from './model/network/helper/csrf.interceptor'; +import {SettingsEntryComponent} from './ui/settings/_abstract/settings-entry/settings-entry.component'; @Injectable() @@ -187,6 +188,7 @@ export function translationsFactory(locale: string) { DuplicateComponent, DuplicatesPhotoComponent, // Settings + SettingsEntryComponent, UserMangerSettingsComponent, DatabaseSettingsComponent, MapSettingsComponent, @@ -218,6 +220,7 @@ export function translationsFactory(locale: string) { {provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true}, {provide: UrlSerializer, useClass: CustomUrlSerializer}, {provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig}, + StringifySortingMethod, NetworkService, ShareService, UserService, diff --git a/src/frontend/app/ui/admin/admin.component.html b/src/frontend/app/ui/admin/admin.component.html index 0969a4e6..98fbd63e 100644 --- a/src/frontend/app/ui/admin/admin.component.html +++ b/src/frontend/app/ui/admin/admin.component.html @@ -31,13 +31,15 @@ id="simplifiedMode" class="switch" name="simplifiedMode" - [switch-off-color]="'warning'" - [switch-on-color]="'primary'" - [switch-inverse]="true" - [switch-off-text]="text.Advanced" - [switch-on-text]="text.Simplified" - [switch-handle-width]="100" - [switch-label-width]="20" + switch-off-color="warning" + switch-on-color="primary" + switch-inverse="true" + switch-off-text="Advanced" + switch-on-text="Simplified" + i18n-switch-off-text + i18n-switch-on-text + switch-handle-width="100" + switch-label-width="20" [(ngModel)]="simplifiedMode"> diff --git a/src/frontend/app/ui/admin/admin.component.ts b/src/frontend/app/ui/admin/admin.component.ts index 272ad3d0..99a3b103 100644 --- a/src/frontend/app/ui/admin/admin.component.ts +++ b/src/frontend/app/ui/admin/admin.component.ts @@ -17,15 +17,11 @@ import {formatDate} from '@angular/common'; }) export class AdminComponent implements OnInit, AfterViewInit { simplifiedMode = true; - text = { - Advanced: 'Advanced', - Simplified: 'Simplified' - }; appVersion = Config.Client.appVersion; versionExtra = ''; upTime = Config.Client.upTime; @ViewChildren('setting') settingsComponents: QueryList; - @ViewChildren('setting', {read: ElementRef}) settingsComponents2: QueryList; + @ViewChildren('setting', {read: ElementRef}) settingsComponentsElemRef: QueryList; contents: ISettingsComponent[] = []; constructor(private _authService: AuthenticationService, @@ -33,8 +29,6 @@ export class AdminComponent implements OnInit, AfterViewInit { public notificationService: NotificationService, @Inject(LOCALE_ID) private locale: string, public i18n: I18n) { - this.text.Advanced = i18n('Advanced'); - this.text.Simplified = i18n('Simplified'); if (Config.Client.buildTime) { this.versionExtra = i18n('Built at') + ': ' + formatDate(Config.Client.buildTime, 'medium', locale); @@ -50,13 +44,13 @@ export class AdminComponent implements OnInit, AfterViewInit { } scrollTo(i: number) { - PageHelper.ScrollY = this.settingsComponents2.toArray()[i].nativeElement.getBoundingClientRect().top + - PageHelper.ScrollY; + PageHelper.ScrollY = this.settingsComponentsElemRef.toArray()[i].nativeElement.getBoundingClientRect().top + + PageHelper.ScrollY; } ngOnInit() { if (!this._authService.isAuthenticated() - || this._authService.user.value.role < UserRoles.Admin) { + || this._authService.user.value.role < UserRoles.Admin) { this._navigation.toLogin(); return; } diff --git a/src/frontend/app/ui/settings/_abstract/abstract.settings.component.css b/src/frontend/app/ui/settings/_abstract/abstract.settings.component.css index 891d3705..798a7957 100644 --- a/src/frontend/app/ui/settings/_abstract/abstract.settings.component.css +++ b/src/frontend/app/ui/settings/_abstract/abstract.settings.component.css @@ -16,3 +16,13 @@ margin-bottom: -4px; } + +.changed-settings input { + border-color: #007bff; + border-width: 1.5px; +} + +.changed-settings label { + color: #007bff; + font-weight: bold; +} diff --git a/src/frontend/app/ui/settings/_abstract/abstract.settings.component.ts b/src/frontend/app/ui/settings/_abstract/abstract.settings.component.ts index 931535e6..2ecfff05 100644 --- a/src/frontend/app/ui/settings/_abstract/abstract.settings.component.ts +++ b/src/frontend/app/ui/settings/_abstract/abstract.settings.component.ts @@ -10,8 +10,29 @@ import {I18n} from '@ngx-translate/i18n-polyfill'; import {Subscription} from 'rxjs'; import {ISettingsComponent} from './ISettingsComponent'; import {WebConfig} from '../../../../../common/config/private/WebConfig'; +import {FormControl} from '@angular/forms'; +interface ConfigState { + value: any; + original: any; + default: any; + readonly: any; + onChange: any; + isEnumType: boolean; + isConfigType: boolean; +} +interface RecursiveState extends ConfigState { + value: any; + original: any; + default: any; + readonly: any; + onChange: any; + isEnumType: any; + isConfigType: any; + + [key: string]: RecursiveState; +} export abstract class SettingsComponent = AbstractSettingsService> implements OnInit, OnDestroy, OnChanges, ISettingsComponent { @@ -20,7 +41,8 @@ export abstract class SettingsComponent{}; - public original: T = {}; - text = { - Enabled: 'Enabled', - Disabled: 'Disabled', - Low: 'Low', - High: 'High' - }; + public states: RecursiveState = {}; + + private _subscription: Subscription = null; private readonly _settingsSubscription: Subscription = null; @@ -50,10 +67,6 @@ export abstract class SettingsComponent { - this.settings = Utils.clone(this.sliceFN(s)); - this.original = Utils.clone(this.settings); + + this.states = Utils.clone(this.sliceFN(s.State)); + const addOriginal = (obj: any) => { + for (const k of Object.keys(obj)) { + if (typeof obj[k].value === 'undefined') { + if (typeof obj[k] === 'object') { + addOriginal(obj[k]); + } + continue; + } + + obj[k].original = Utils.clone(obj[k].value); + obj[k].onChange = this.onOptionChange; + } + }; + addOriginal(this.states); this.ngOnChanges(); }; @@ -104,12 +131,32 @@ export abstract class SettingsComponent { setTimeout(() => { - this.changed = !this.settingsSame(this.settings, this.original); + const settingsSame = (state: RecursiveState): boolean => { + if (typeof state === 'undefined') { + return true; + } + if (typeof state.original === 'object') { + return Utils.equalsFilter(state.original, state.value); + } + if (typeof state.original !== 'undefined') { + return state.value === state.original; + } + const keys = Object.keys(state); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + if (settingsSame(state[key]) === false) { + return false; + } + } + + return true; + }; + + this.changed = !settingsSame(this.states); }, 0); - } + }; ngOnInit() { if (!this._authService.isAuthenticated() || @@ -121,9 +168,8 @@ export abstract class SettingsComponent { - this.testSettingChanges(); + this.onOptionChange(); }); - } ngOnChanges(): void { @@ -146,11 +192,28 @@ export abstract class SettingsComponent{}; + + const add = (obj: any, to: any): void => { + for (const key of Object.keys(obj)) { + to[key] = {}; + if (obj[key].isConfigType) { + return add(obj[key], to[key]); + } + to[key] = obj[key].value; + } + }; + add(this.states, ret); + return ret; + + } + public async save() { this.inProgress = true; this.error = ''; try { - await this._settingsService.updateSettings(this.settings); + await this._settingsService.updateSettings(this.stateToSettings()); await this.getSettings(); this.notification.success(this.Name + ' ' + this.i18n('settings saved'), this.i18n('Success')); this.inProgress = false; @@ -166,11 +229,11 @@ export abstract class SettingsComponent +
+ +
+ + {{description}} + +
+
+
+ +
+ + {{description}} + + +
+
+ +
+ +
+ + + + {{description}} + + +
+
+ diff --git a/src/frontend/app/ui/settings/_abstract/settings-entry/settings-entry.component.ts b/src/frontend/app/ui/settings/_abstract/settings-entry/settings-entry.component.ts new file mode 100644 index 00000000..234e0ddc --- /dev/null +++ b/src/frontend/app/ui/settings/_abstract/settings-entry/settings-entry.component.ts @@ -0,0 +1,187 @@ +import {Component, forwardRef, Input, OnChanges} from '@angular/core'; +import {I18n} from '@ngx-translate/i18n-polyfill'; +import {ControlValueAccessor, FormControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, ValidationErrors, Validator} from '@angular/forms'; +import {Utils} from '../../../../../../common/Utils'; +import {propertyTypes} from 'typeconfig/common'; + +@Component({ + selector: 'app-settings-entry', + templateUrl: './settings-entry.component.html', + styleUrls: ['./settings-entry.settings.component.css'], + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => SettingsEntryComponent), + multi: true + }, + + { + provide: NG_VALIDATORS, + useExisting: forwardRef(() => SettingsEntryComponent), + multi: true + } + ] +}) +export class SettingsEntryComponent implements ControlValueAccessor, Validator, OnChanges { + + @Input() name: string; + @Input() required: boolean; + @Input() optionMap: (v: { key: number, value: string }) => { key: number, value: string }; + @Input() placeholder: string; + @Input() options: { key: number | string, value: number | string }[]; + @Input() simplifiedMode = false; + @Input() description: boolean; + state: { + isEnumType: boolean, + isConfigType: boolean, + default: any, value: any, min?: number, max?: number, + type: propertyTypes, arrayType: propertyTypes, + original: any, readonly?: boolean + }; + isNumberArray = false; + isNumber = false; + type = 'text'; + _options: { key: number | string; value: string | number; }[] = []; + title: string; + idName: string; + _disabled: boolean; + private readonly GUID = Utils.GUID(); + + + // value: { default: any, setting: any, original: any, readonly?: boolean, onChange: () => void }; + + constructor(private i18n: I18n) { + } + + get changed(): boolean { + if (this._disabled) { + return false; + } + if (this.state.type === 'array') { + return !Utils.equalsFilter(this.state.value, this.state.default); + } + return this.state.value !== this.state.default; + } + + get shouldHide(): boolean { + if (Array.isArray(this.state.value)) { + return this.simplifiedMode && Utils.equalsFilter(this.state.value, this.state.default) + && Utils.equalsFilter(this.state.original, this.state.default); + } + return this.simplifiedMode && this.state.value === this.state.default && this.state.original === this.state.default; + } + + + get PlaceHolder(): string { + return this.placeholder || this.state.default; + } + + get defaultStr(): string { + + if (this.state.type === 'array' && this.state.arrayType === 'string') { + return (this.state.default || []).join(';'); + } + + return this.state.default; + } + + get value(): any { + if (this.state.type === 'array' && + (this.state.arrayType === 'string' || this.isNumberArray)) { + return this.state.value.join(';'); + } + + return this.state.value; + } + + set value(value: any) { + if (this.state.type === 'array' && + (this.state.arrayType === 'string' || this.isNumberArray)) { + value = value.replace(new RegExp(',', 'g'), ';'); + value = value.replace(new RegExp(' ', 'g'), ';'); + this.state.value = value.split(';'); + if (this.isNumberArray) { + this.state.value = this.state.value.map((v: string) => parseFloat(v)).filter((v: number) => !isNaN(v)); + } + return; + } + this.state.value = value; + if (this.isNumber) { + this.state.value = parseFloat(value); + } + } + + setDisabledState?(isDisabled: boolean): void { + this._disabled = isDisabled; + } + + ngOnChanges(): void { + if (!this.state) { + return; + } + if (this.options) { + this.state.isEnumType = true; + } + this.title = ''; + if (this.state.readonly) { + this.title = this.i18n('readonly') + ', '; + } + this.title += this.i18n('default value') + ': ' + this.defaultStr; + if (this.name) { + this.idName = this.GUID + this.name.toLowerCase().replace(new RegExp(' ', 'gm'), '-'); + } + this.isNumberArray = this.state.arrayType === 'unsignedInt' || + this.state.arrayType === 'integer' || this.state.arrayType === 'float' || this.state.arrayType === 'positiveFloat'; + this.isNumber = this.state.type === 'unsignedInt' || + this.state.type === 'integer' || this.state.type === 'float' || this.state.type === 'positiveFloat'; + if (this.state.isEnumType) { + if (this.options) { + this._options = this.options; + } else { + if (this.optionMap) { + this._options = Utils.enumToArray(this.state.type).map(this.optionMap); + } else { + this._options = Utils.enumToArray(this.state.type); + } + } + } + + if (this.isNumber) { + this.type = 'number'; + } else if (this.state.type === 'password') { + this.type = 'password'; + } else { + this.type = 'text'; + } + } + + validate(control: FormControl): ValidationErrors { + if (!this.required || (this.state && this.state.value && this.state.value !== '')) { + return null; + } + return {required: true}; + } + + public onChange(value: any): void { + } + + public onTouched(): void { + } + + public writeValue(obj: any): void { + this.state = obj; + this.ngOnChanges(); + } + + public registerOnChange(fn: any): void { + this.onChange = fn; + } + + public registerOnTouched(fn: any): void { + this.onTouched = fn; + } + +} + + + diff --git a/src/frontend/app/ui/settings/_abstract/settings-entry/settings-entry.settings.component.css b/src/frontend/app/ui/settings/_abstract/settings-entry/settings-entry.settings.component.css new file mode 100644 index 00000000..07baa5aa --- /dev/null +++ b/src/frontend/app/ui/settings/_abstract/settings-entry/settings-entry.settings.component.css @@ -0,0 +1,11 @@ + +.changed-settings input, .changed-settings select { + border-color: #007bff; + border-width: 1.5px; +} + +.changed-settings label { + color: #007bff; + font-weight: bold; +} + diff --git a/src/frontend/app/ui/settings/basic/basic.settings.component.html b/src/frontend/app/ui/settings/basic/basic.settings.component.html index 49bfd860..97fa2dca 100644 --- a/src/frontend/app/ui/settings/basic/basic.settings.component.html +++ b/src/frontend/app/ui/settings/basic/basic.settings.component.html @@ -7,91 +7,82 @@ -
- -
- -
-
+ + -
- -
- - Server will accept connections from this IPv6 or IPv4 address. -
-
-
- -
- - Port number. Port 80 is usually what you need. -
-
+ + -
- -
- - Images are loaded from this folder (read permission required) -
-
-
- -
- - Thumbnails, coverted photos, videos will be stored here (write permission required) -
-
+ + -
- -
- - If you access the page form local network its good to know the public - url for creating sharing link - -
-
-
- -
- - If you access the gallery under a sub url (like: - http://mydomain.com/myGallery), set it here. If not working you might miss the '/' from the beginning of the - url. - -
-
+ + + + + + + + + + + + + + +