1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-12-12 11:15:22 +02:00

Merge pull request #135 from bpatrik/develop

implementing advanced settings
This commit is contained in:
Patrik J. Braun 2020-02-04 20:42:01 +01:00 committed by GitHub
commit 399dbfb53c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
47 changed files with 3248 additions and 2819 deletions

8
package-lock.json generated
View File

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

View File

@ -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"
},

View File

@ -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<PrivateConfigClass>(null, <any>originalConf.toJSON({attachDefaults: true}));
const message = new Message<PrivateConfigClass>(null, <any>originalConf.toJSON({
attachState: true,
attachVolatile: true
}));
res.json(message);
}

View File

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

View File

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

View File

@ -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 = <IConfigClass & ClientConfig.Config>(new ClientConfig.Config());
constructor() {
if (!this.Server.sessionSecret || this.Server.sessionSecret.length === 0) {

View File

@ -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();

View File

@ -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();

View File

@ -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();

View File

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

View File

@ -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">
</bSwitch>
</div>

View File

@ -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<ISettingsComponent>;
@ViewChildren('setting', {read: ElementRef}) settingsComponents2: QueryList<ElementRef>;
@ViewChildren('setting', {read: ElementRef}) settingsComponentsElemRef: QueryList<ElementRef>;
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;
}

View File

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

View File

@ -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<T extends { [key: string]: any }, S extends AbstractSettingsService<T> = AbstractSettingsService<T>>
implements OnInit, OnDestroy, OnChanges, ISettingsComponent {
@ -20,7 +41,8 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
public simplifiedMode = true;
@ViewChild('settingsForm', {static: true})
form: HTMLFormElement;
form: FormControl;
@Output()
hasAvailableSettings = true;
@ -28,14 +50,9 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
public inProgress = false;
public error: string = null;
public changed = false;
public settings: T = <T>{};
public original: T = <T>{};
text = {
Enabled: 'Enabled',
Disabled: 'Disabled',
Low: 'Low',
High: 'High'
};
public states: RecursiveState = <any>{};
private _subscription: Subscription = null;
private readonly _settingsSubscription: Subscription = null;
@ -50,10 +67,6 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
this._settingsSubscription = this._settingsService.Settings.subscribe(this.onNewSettings);
this.onNewSettings(this._settingsService._settingsService.settings.value);
}
this.text.Enabled = i18n('Enabled');
this.text.Disabled = i18n('Disabled');
this.text.Low = i18n('Low');
this.text.High = i18n('High');
}
@ -70,8 +83,22 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
}
onNewSettings = (s: WebConfig) => {
this.settings = Utils.clone(this.sliceFN(s));
this.original = Utils.clone(this.settings);
this.states = Utils.clone(<any>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<T extends { [key: string]: any }, S exte
return true;
}
public testSettingChanges() {
// TODO: fix after this issue is fixed: https://github.com/angular/angular/issues/24818
onOptionChange = () => {
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<T extends { [key: string]: any }, S exte
// TODO: fix after this issue is fixed: https://github.com/angular/angular/issues/24818
this._subscription = this.form.valueChanges.subscribe(() => {
this.testSettingChanges();
this.onOptionChange();
});
}
ngOnChanges(): void {
@ -146,11 +192,28 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
this.getSettings();
}
stateToSettings(): T {
const ret: T = <any>{};
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<T extends { [key: string]: any }, S exte
return false;
}
private async getSettings() {
await this._settingsService.getSettings();
this.changed = false;
}
}

View File

@ -0,0 +1,67 @@
<ng-container *ngIf="state">
<div class="form-group row" *ngIf="!state.isEnumType && state.type !== 'boolean'"
[class.changed-settings]="changed"
[hidden]="shouldHide">
<label class="col-md-2 control-label" [for]="idName">{{name}}</label>
<div class="col-md-10">
<input [type]="type" [min]="state.min" [max]="state.max" class="form-control" [placeholder]="PlaceHolder"
[title]="title"
[(ngModel)]="value"
(ngModelChange)="onChange($event)"
[name]="idName"
[disabled]="state.readonly || _disabled"
[id]="idName"
required="required">
<small class="form-text text-muted" *ngIf="description">{{description}}
</small>
</div>
</div>
<div class="form-group row" *ngIf="state.isEnumType === true"
[class.changed-settings]="changed"
[hidden]="shouldHide">
<label class="col-md-2 control-label" [for]="idName">{{name}}</label>
<div class="col-md-10">
<select [id]="idName"
[name]="idName"
[title]="title"
(ngModelChange)="onChange($event)"
[disabled]="state.readonly || _disabled"
class="form-control" [(ngModel)]="state.value">
<option *ngFor="let opt of _options" [ngValue]="opt.key">{{opt.value}}
</option>
</select>
<small class="form-text text-muted" *ngIf="description">{{description}}
</small>
<ng-content></ng-content>
</div>
</div>
<div class="form-group row" *ngIf="state.type === 'boolean'"
[class.changed-settings]="changed"
[hidden]="shouldHide">
<label class="col-md-2 control-label" [for]="idName">{{name}}</label>
<div class="col-md-10">
<bSwitch
class="switch"
[id]="idName"
[name]="idName"
[title]="title"
[disabled]="state.readonly || _disabled"
switch-on-color="primary"
switch-inverse="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
switch-handle-width="100"
switch-label-width="20"
(ngModelChange)="onChange($event)"
[(ngModel)]="state.value">
</bSwitch>
<small class="form-text text-muted" *ngIf="description">{{description}}
</small>
<ng-content></ng-content>
</div>
</div>
</ng-container>

View File

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

View File

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

View File

@ -7,91 +7,82 @@
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<div class="form-group row" [hidden]="simplifiedMode">
<label class="col-md-2 control-label" for="applicationTitle" i18n>Page title</label>
<div class="col-md-10">
<input type="text" class="form-control" placeholder="Pigallery 2"
id="applicationTitle"
[(ngModel)]="settings.applicationTitle"
name="applicationTitle" required>
</div>
</div>
<app-settings-entry
name="Page title"
[ngModel]="states.applicationTitle"
i18n-name
required="true"
[simplifiedMode]="simplifiedMode">
</app-settings-entry>
<div class="form-group row" [hidden]="simplifiedMode">
<label class="col-md-2 control-label" for="host" i18n>Host</label>
<div class="col-md-10">
<input type="text" class="form-control" placeholder="0.0.0.0"
id="host"
[(ngModel)]="settings.host"
name="host" required>
<small class="form-text text-muted" i18n>Server will accept connections from this IPv6 or IPv4 address.</small>
</div>
</div>
<div class="form-group row" [hidden]="simplifiedMode">
<label class="col-md-2 control-label" for="port" i18n>Port</label>
<div class="col-md-10">
<input type="number" class="form-control" placeholder="80"
id="port"
min="0"
step="1"
max="65535"
[(ngModel)]="settings.port"
name="port" required>
<small class="form-text text-muted" i18n>Port number. Port 80 is usually what you need.</small>
</div>
</div>
<app-settings-entry
name="Host"
[ngModel]="states.host"
description="Server will accept connections from this IPv6 or IPv4 address."
i18n-description i18n-name
placeholder="0.0.0.0"
required="true"
[simplifiedMode]="simplifiedMode">
</app-settings-entry>
<div class="form-group row">
<label class="col-md-2 control-label" for="imagesFolder" i18n>Images folder</label>
<div class="col-md-10">
<input type="text" class="form-control" placeholder="path"
id="imagesFolder"
[(ngModel)]="settings.imagesFolder"
name="imagesFolder" required>
<small class="form-text text-muted" i18n>Images are loaded from this folder (read permission required)</small>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 control-label" for="tempFolder" i18n>Temp folder</label>
<div class="col-md-10">
<input type="text" class="form-control" placeholder="path"
id="tempFolder"
[(ngModel)]="settings.tempFolder"
name="tempFolder" required>
<small class="form-text text-muted" i18n>Thumbnails, coverted photos, videos will be stored here (write permission required)</small>
</div>
</div>
<app-settings-entry
name="Port"
description="Port number. Port 80 is usually what you need."
i18n-description i18n-name
[ngModel]="states.port"
required="true"
[simplifiedMode]="simplifiedMode">
</app-settings-entry>
<div class="form-group row" [hidden]="simplifiedMode">
<label class="col-md-2 control-label" for="publicUrl" i18n>Page public url</label>
<div class="col-md-10">
<input type="url" class="form-control" placeholder="{{urlPlaceholder}}"
id="publicUrl"
[(ngModel)]="settings.publicUrl"
(change)="onUrlChanged()"
name="publicUrl">
<small class="form-text text-muted" i18n>If you access the page form local network its good to know the public
url for creating sharing link
</small>
</div>
</div>
<div class="form-group row" [hidden]="simplifiedMode">
<label class="col-md-2 control-label" for="urlBase" i18n>Url Base</label>
<div class="col-md-10">
<input type="url" class="form-control" placeholder="/myGallery"
id="urlBase"
[(ngModel)]="settings.urlBase"
(change)="onUrlBaseChanged()"
name="urlBase">
<small class="form-text text-muted" i18n>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.
</small>
</div>
</div>
<app-settings-entry
name="Images folder"
description="Images are loaded from this folder (read permission required)"
placeholder="path"
i18n-description i18n-name
required
[ngModel]="states.imagesFolder">
</app-settings-entry>
<app-settings-entry
name="Temp folder"
description="Thumbnails, converted photos, videos will be stored here (write
permission required)"
placeholder="path"
i18n-description i18n-name
required="true"
[ngModel]="states.tempFolder">
</app-settings-entry>
<app-settings-entry
name="Page public url"
[(ngModel)]="states.publicUrl"
(change)="onUrlChanged()"
description="If you access the page form local network its good to know the public
url for creating sharing link"
i18n-description i18n-name
[placeholder]="urlPlaceholder"
[simplifiedMode]="simplifiedMode">
</app-settings-entry>
<app-settings-entry
name="Url Base"
description="If you access the gallery under a sub url (like:
http://mydomain.com/myGallery), set it here. If it is not working you might miss the '/' from the beginning
of the
url."
placeholder="/myGallery"
i18n-description i18n-name
[ngModel]="states.urlBase"
(change)="onUrlBaseChanged()"
[simplifiedMode]="simplifiedMode">
</app-settings-entry>
<div *ngIf="urlError===true" class="alert alert-warning" role="alert" i18n>
The public url and the url base are not matching. Some of the functionality might not work.

View File

@ -46,7 +46,7 @@ export class BasicSettingsComponent extends SettingsComponent<BasicConfigDTO> {
}
calcBaseUrl(): string {
const url = this.settings.publicUrl.replace(new RegExp('\\\\', 'g'), '/')
const url = this.states.publicUrl.value.replace(new RegExp('\\\\', 'g'), '/')
.replace(new RegExp('http://', 'g'), '')
.replace(new RegExp('https://', 'g'), '');
if (url.indexOf('/') !== -1) {
@ -57,23 +57,24 @@ export class BasicSettingsComponent extends SettingsComponent<BasicConfigDTO> {
}
checkUrlError() {
this.urlError = this.settings.urlBase !== this.calcBaseUrl();
this.urlError = this.states.urlBase.value !== this.calcBaseUrl();
}
onUrlChanged() {
console.log('called');
if (this.urlBaseChanged === false) {
this.settings.urlBase = this.calcBaseUrl();
this.states.urlBase.value = this.calcBaseUrl();
} else {
this.checkUrlError();
}
}
onUrlBaseChanged() {
onUrlBaseChanged = () => {
this.urlBaseChanged = true;
this.checkUrlError();
}
};
}

View File

@ -6,71 +6,68 @@
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<app-settings-entry
name="Type"
[ngModel]="states.type"
i18n-name
required="true">
<small *ngIf="states.type.value == DatabaseType.mysql"
class="form-text text-muted" i18n>Install manually mysql node module to use mysql (npm install mysql)
</small>
</app-settings-entry>
<div class="form-group row">
<label class="col-md-2 control-label" for="Type" i18n>Type</label>
<div class="col-md-10">
<select name="Type" id="Type"
class="form-control" [(ngModel)]="settings.type" required>
<option *ngFor="let type of types" [ngValue]="type.key">{{type.value}}
</option>
</select>
<small *ngIf="settings.type == DatabaseType.mysql"
class="form-text text-muted" i18n>Install manually mysql node module to use mysql (npm install mysql)
</small>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 control-label" for="dbFolder" i18n>Database folder</label>
<div class="col-md-10">
<input type="text" class="form-control" placeholder="db"
[(ngModel)]="settings.dbFolder" id="dbFolder" name="dbFolder" required>
<small class="form-text text-muted" i18n>
All file-based data will be stored here (sqlite database, user database in case of memory db, job history
data)
</small>
</div>
</div>
<app-settings-entry
name="Database folder"
description="All file-based data will be stored here (sqlite database, user database in case of memory db, job history data)"
[ngModel]="states.dbFolder"
i18n-name i18n-description
required="true">
</app-settings-entry>
<ng-container *ngIf="settings.type == DatabaseType.mysql">
<div class="form-group row">
<label class="col-md-2 control-label" for="mysql_host" i18n>Host</label>
<div class="col-md-10">
<input type="text" class="form-control" placeholder="localhost"
[(ngModel)]="settings.mysql.host" id="mysql_host" name="mysql_host" required>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 control-label" for="mysql_port" i18n>Port</label>
<div class="col-md-10">
<input type="number" class="form-control" placeholder="3306" min="0" max="65535"
[(ngModel)]="settings.mysql.port" id="mysql_port" name="mysql_port" required>
</div>
</div>
<ng-container *ngIf="states.type.value == DatabaseType.mysql">
<app-settings-entry
name="Host"
[ngModel]="states.mysql.host"
i18n-name
required="true">
</app-settings-entry>
<app-settings-entry
name="Port"
[ngModel]="states.mysql.port"
i18n-name
required="true">
</app-settings-entry>
<app-settings-entry
name="Database"
[ngModel]="states.mysql.database"
i18n-name
required="true">
</app-settings-entry>
<app-settings-entry
name="Username"
[ngModel]="states.mysql.username"
placeholder="username"
i18n-name
required="true">
</app-settings-entry>
<app-settings-entry
name="Password"
[ngModel]="states.mysql.password"
placeholder="password"
i18n-name
required="true">
</app-settings-entry>
<div class="form-group row">
<label class="col-md-2 control-label" for="mysql_database" i18n>Database</label>
<div class="col-md-10">
<input type="text" class="form-control" placeholder="pigallery2"
[(ngModel)]="settings.mysql.database" id="mysql_database" name="mysql_database" required>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 control-label" for="mysql_username" i18n>Username</label>
<div class="col-md-10">
<input type="text" class="form-control" placeholder="username"
[(ngModel)]="settings.mysql.username" id="mysql_username" name="mysql_username" required>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 control-label" for="mysql_password" i18n>Password</label>
<div class="col-md-10">
<input type="password" class="form-control" placeholder="password"
[(ngModel)]="settings.mysql.password" id="mysql_password" name="mysql_password" required>
</div>
</div>
</ng-container>

View File

@ -1,62 +1,49 @@
<form #settingsForm="ngForm">
<div class="card mb-4"
[ngClass]="settings.enabled && !_settingsService.isSupported()?'panel-warning':''">
[ngClass]="states.enabled.value && !_settingsService.isSupported()?'panel-warning':''">
<h5 class="card-header">
{{Name}}
<div class="switch-wrapper">
<div class="switch-wrapper"
[class.changed-settings]="states.enabled.value != states.enabled.default">
<bSwitch
class="switch"
name="enabled"
[switch-on-color]="'success'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-disabled]="inProgress || (!settings.enabled && !_settingsService.isSupported())"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.enabled">
switch-on-color="success"
switch-inverse="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
[switch-disabled]="inProgress || (!states.enabled.value && !_settingsService.isSupported())"
switch-handle-width="100"
switch-label-width="20"
[(ngModel)]="states.enabled.value">
</bSwitch>
</div>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<ng-container *ngIf="settings.enabled || _settingsService.isSupported()">
<div class="form-group row">
<label class="col-md-2 control-label" for="autocompleteEnabled" i18n>Override keywords</label>
<div class="col-md-10">
<bSwitch
id="autocompleteEnabled"
class="switch"
name="autocompleteEnabled"
[switch-on-color]="'primary'"
[switch-disabled]="!settings.enabled"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.keywordsToPersons">
</bSwitch>
<small class="form-text text-muted" i18n>If a photo has the same face (person) name and keyword, the app removes the duplicate, keeping the face only.</small>
</div>
</div>
<ng-container *ngIf="states.enabled.value || _settingsService.isSupported()">
<div class="form-group row">
<label class="col-md-2 control-label" for="writeAccessMinRole" i18n>Face starring right</label>
<div class="col-md-10">
<select class="form-control" [(ngModel)]="settings.writeAccessMinRole" name="writeAccessMinRole" id="writeAccessMinRole" required>
<option *ngFor="let repository of userRoles" [value]="repository.key">{{repository.value}}
</option>
</select>
<small class="form-text text-muted" i18n>Required minimum right to start (favourite) a face.</small>
</div>
</div>
<app-settings-entry
name="Override keywords"
[ngModel]="states.keywordsToPersons"
description="If a photo has the same face (person) name and keyword, the app removes the duplicate, keeping the face only."
i18n-name i18n-description>
</app-settings-entry>
<app-settings-entry
name="Face starring right"
[ngModel]="states.writeAccessMinRole"
description="Required minimum right to start (favourite) a face."
i18n-name i18n-description>
</app-settings-entry>
</ng-container>
<div class="panel-info" *ngIf="(!settings.enabled && !_settingsService.isSupported())" i18n>
<div class="panel-info" *ngIf="(!states.enabled.value && !_settingsService.isSupported())" i18n>
Faces are not supported with these settings.
</div>
<button class="btn btn-success float-right"

View File

@ -7,84 +7,66 @@
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<ng-container *ngIf="!simplifiedMode">
<div class="form-group row">
<label class="col-md-2 control-label" for="cachedFolderTimeout" i18n>Index cache timeout [ms]</label>
<div class="col-md-10">
<input type="number" class="form-control" placeholder="36000"
id="cachedFolderTimeout"
min="0"
step="1"
[(ngModel)]="settings.cachedFolderTimeout"
name="cachedFolderTimeout" required>
<small class="form-text text-muted" i18n>If there was no indexing in this time, it reindexes. (skipped if
indexes are in DB and sensitivity is low)
</small>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 control-label" for="folderPreviewSize" i18n>Sub folder preview size</label>
<div class="col-md-10">
<input type="number" class="form-control" placeholder="1"
id="folderPreviewSize"
min="0"
step="1"
[(ngModel)]="settings.folderPreviewSize"
name="folderPreviewSize" required>
<small class="form-text text-muted" i18n>Reads this many photos from sub folders</small>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 control-label" for="reIndexingSensitivity" i18n>Folder reindexing sensitivity</label>
<div class="col-md-10">
<select id="reIndexingSensitivity" class="form-control" [(ngModel)]="settings.reIndexingSensitivity"
name="reIndexingSensitivity" required>
<option *ngFor="let type of types" [ngValue]="type.key">{{type.value}}
</option>
</select>
<small
class="form-text text-muted"
i18n>Set the reindexing sensitivity. High value check the folders for change more often.
</small>
</div>
</div>
<app-settings-entry
name="Index cache timeout [ms]"
description="If there was no indexing in this time, it reindexes. (skipped if indexes are in DB and sensitivity is low)"
i18n-description i18n-name
[ngModel]="states.cachedFolderTimeout"
required="true">
</app-settings-entry>
<app-settings-entry
name="Sub folder preview size"
description="Reads this many photos from sub folders."
i18n-description i18n-name
[ngModel]="states.folderPreviewSize"
required="true">
</app-settings-entry>
<app-settings-entry
name="Folder reindexing sensitivity"
[ngModel]="states.reIndexingSensitivity"
description="Set the reindexing sensitivity. High value check the folders for change more often."
i18n-description i18n-name
required="true">
</app-settings-entry>
<app-settings-entry
name="Exclude Folder List"
i18n-name
placeholder="/media/images/family/private;private;family/private"
[ngModel]="states.excludeFolderList"
required="true">
<small class="form-text text-muted">
<ng-container i18n>Folders to exclude from indexing</ng-container>
<br/>
<ng-container
i18n>';' separated strings. 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.
</ng-container>
</small>
</app-settings-entry>
<app-settings-entry
name="Exclude File List"
i18n-name
placeholder=".ignore;.pg2ignore"
[ngModel]="states.excludeFileList"
required="true">
<small class="form-text text-muted">
<ng-container i18n>Files that mark a folder to be excluded from indexing</ng-container>
<br/>
<ng-container
i18n>';' separated strings. Any folder that contains a file with this name will be excluded from
indexing.
</ng-container>
</small>
</app-settings-entry>
<div class="form-group row">
<label class="col-md-2 control-label" for="excludeFolderList" i18n>Exclude Folder List</label>
<div class="col-md-10">
<input type="text" class="form-control" placeholder="/media/images/family/private;private;family/private"
id="excludeFolderList"
[(ngModel)]="excludeFolderList"
name="excludeFolderList" required>
<small class="form-text text-muted">
<ng-container i18n>Folders to exclude from indexing</ng-container>
<br/>
<ng-container
i18n>';' separated strings. 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.
</ng-container>
</small>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 control-label" for="excludeFileList" i18n>Exclude File List</label>
<div class="col-md-10">
<input type="text" class="form-control" placeholder=".ignore;.pg2ignore"
id="excludeFileList"
[(ngModel)]="excludeFileList"
name="excludeFileList" required>
<small class="form-text text-muted">
<ng-container i18n>Files that mark a folder to be excluded from indexing</ng-container>
<br/>
<ng-container
i18n>';' separated strings. Any folder that contains a file with this name will be excluded from
indexing.
</ng-container>
</small>
</div>
</div>
<button class="btn btn-success float-right"

View File

@ -49,21 +49,6 @@ export class IndexingSettingsComponent extends SettingsComponent<ServerConfig.In
return this.jobsService.progress.value[JobDTO.getHashName(DefaultsJobs[DefaultsJobs.Indexing])];
}
get excludeFolderList(): string {
return this.settings.excludeFolderList.join(';');
}
set excludeFolderList(value: string) {
this.settings.excludeFolderList = value.split(';');
}
get excludeFileList(): string {
return this.settings.excludeFileList.join(';');
}
set excludeFileList(value: string) {
this.settings.excludeFileList = value.split(';');
}
ngOnDestroy() {
super.ngOnDestroy();

View File

@ -83,7 +83,7 @@
<div class="col-md-10">
<select class="form-control" [(ngModel)]="schedule.trigger.afterScheduleName"
[name]="'triggerAfter'+i" required>
<ng-container *ngFor="let sch of settings.scheduled">
<ng-container *ngFor="let sch of states.scheduled.value">
<option *ngIf="sch.name !== schedule.name"
[ngValue]="sch.name">{{sch.name}}
</option>
@ -101,7 +101,7 @@
<div class="col-md-10">
<app-timestamp-datepicker
[name]="'triggerTime'+i"
(timestampChange)="testSettingChanges()"
(timestampChange)="onOptionChange()"
[(timestamp)]="schedule.trigger.time"></app-timestamp-datepicker>
</div>
</div>
@ -121,7 +121,7 @@
</select>
<app-timestamp-timepicker
[name]="'atTime'+i"
(timestampChange)="testSettingChanges()"
(timestampChange)="onOptionChange()"
[(timestamp)]="schedule.trigger.atTime"></app-timestamp-timepicker>
</div>
</div>
@ -132,12 +132,13 @@
class="switch"
[name]="'allowParallelRun'+'_'+i"
[id]="'allowParallelRun'+'_'+i"
[switch-on-color]="'primary'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-handle-width]="100"
[switch-label-width]="20"
switch-on-color="primary"
switch-inverse="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text i18n-switch-on-text
switch-handle-width="100"
switch-label-width="20"
[(ngModel)]="schedule.allowParallelRun">
</bSwitch>
<small class="form-text text-muted"
@ -164,12 +165,13 @@
class="switch"
[name]="configEntry.id+'_'+i"
[id]="configEntry.id+'_'+i"
[switch-on-color]="'primary'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-handle-width]="100"
[switch-label-width]="20"
switch-on-color="primary"
switch-inverse="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text i18n-switch-on-text
switch-handle-width="100"
switch-label-width="20"
[(ngModel)]="schedule.config[configEntry.id]">
</bSwitch>
</ng-container>

View File

@ -100,7 +100,7 @@ export class JobsSettingsComponent extends SettingsComponent<ServerConfig.JobCon
}
remove(schedule: JobScheduleDTO) {
this.settings.scheduled.splice(this.settings.scheduled.indexOf(schedule), 1);
this.states.scheduled.value.splice(this.states.scheduled.value.indexOf(schedule), 1);
}
jobTypeChanged(schedule: JobScheduleDTO) {
@ -163,17 +163,17 @@ export class JobsSettingsComponent extends SettingsComponent<ServerConfig.JobCon
return curr && curr.trigger.type === JobTriggerType.after && prev && prev.name === curr.trigger.afterScheduleName;
}
public sortedSchedules() {
return this.settings.scheduled.slice().sort((a, b) => {
return this.getNextRunningDate(a, this.settings.scheduled) - this.getNextRunningDate(b, this.settings.scheduled);
public sortedSchedules(): JobScheduleDTO[] {
return this.states.scheduled.value.slice().sort((a: JobScheduleDTO, b: JobScheduleDTO) => {
return this.getNextRunningDate(a, this.states.scheduled.value) - this.getNextRunningDate(b, this.states.scheduled.value);
});
}
addNewJob() {
const jobName = this.newSchedule.jobName;
const count = this.settings.scheduled.filter(s => s.jobName === jobName).length;
const count = this.states.scheduled.value.filter((s: JobScheduleDTO) => s.jobName === jobName).length;
this.newSchedule.name = count === 0 ? jobName : this.backendTextService.getJobName(jobName) + ' ' + (count + 1);
this.settings.scheduled.push(this.newSchedule);
this.states.scheduled.value.push(this.newSchedule);
this.jobModal.hide();
}

View File

@ -6,56 +6,45 @@
<bSwitch
class="switch"
name="enabled"
[switch-on-color]="'success'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
switch-on-color="success"
switch-inverse="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
[switch-disabled]="inProgress"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.enabled">
switch-handle-width="100"
switch-label-width="20"
[(ngModel)]="states.enabled.value">
</bSwitch>
</div>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<ng-container *ngIf="settings.enabled">
<div class="form-group row" [hidden]="simplifiedMode">
<label class="col-md-2 control-label" for="enableOnScrollRendering" i18n>Use image markers</label>
<div class="col-md-10">
<bSwitch
id="enableOnScrollRendering"
class="switch"
name="enableOnScrollRendering"
[switch-on-color]="'primary'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.useImageMarkers">
</bSwitch>
<small class="form-text text-muted" i18n>
Map will use thumbnail images as markers instead of the default pin.
</small>
</div>
</div>
<ng-container *ngIf="states.enabled.value">
<div class="form-group row">
<label class="col-md-2 control-label" for="mapProvider" i18n>Map provider</label>
<div class="col-md-10">
<select name="mapProvider" id="mapProvider"
[disabled]="!settings.enabled"
class="form-control" [(ngModel)]="settings.mapProvider" required>
<option *ngFor="let type of mapProviders" [ngValue]="type.key">{{type.value}}
</option>
</select>
</div>
</div>
<div class="container custom-layer-container" *ngIf="settings.mapProvider === MapProviders.Custom">
<app-settings-entry
name="Use image markers"
description="Map will use thumbnail images as markers instead of the default pin."
i18n-name i18n-description=""
[ngModel]="states.useImageMarkers"
[simplifiedMode]="simplifiedMode">
</app-settings-entry>
<app-settings-entry
name="Map provider"
i18n-name
[ngModel]="states.mapProvider"
required="true">
</app-settings-entry>
<div class="container custom-layer-container" *ngIf="states.mapProvider.value === MapProviders.Custom">
<table class="table table-hover">
<thead>
<tr>
@ -64,7 +53,7 @@
<th></th>
</tr>
</thead>
<tr *ngFor="let layer of settings.customLayers; let i = index">
<tr *ngFor="let layer of states.customLayers.value; let i = index">
<td><input type="text" class="form-control" placeholder="Street"
[(ngModel)]="layer.name"
[name]="'tileName-'+i" [id]="'tileName-'+i" required></td>
@ -74,8 +63,8 @@
[name]="'tileUrl-'+i" [id]="'tileUrl-'+i" required>
</td>
<td>
<button [disabled]="settings.customLayers.length == 1" (click)="removeLayer(layer)"
[ngClass]="settings.customLayers.length > 1? 'btn-danger':'btn-secondary'"
<button [disabled]="states.customLayers.value.length == 1" (click)="removeLayer(layer)"
[ngClass]="states.customLayers.value.length > 1? 'btn-danger':'btn-secondary'"
class="btn float-right">
<span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span>
</button>
@ -95,18 +84,19 @@
</div>
</div>
<div class="form-group row" *ngIf="settings.mapProvider === MapProviders.Mapbox">
<label class="col-md-2 control-label" for="mapboxAccessToken" i18n>Mapbox access token</label>
<div class="col-md-10">
<input type="text" class="form-control" placeholder="Mapbox access token"
[(ngModel)]="settings.mapboxAccessToken"
name="mapboxAccessToken" id="mapboxAccessToken" required>
<small class="form-text text-muted">
<ng-container i18n>MapBox needs an access token to work, create one at</ng-container>
&nbsp;<a href="https://www.mapbox.com">https://www.mapbox.com</a>.
</small>
</div>
</div>
<app-settings-entry
*ngIf="states.mapProvider.value === MapProviders.Mapbox"
name="Mapbox access token"
i18n-name
placeholder="Mapbox access token"
[ngModel]="states.mapboxAccessToken"
required="true">
<small class="form-text text-muted">
<ng-container i18n>MapBox needs an access token to work, create one at</ng-container>
&nbsp;<a href="https://www.mapbox.com">https://www.mapbox.com</a>.
</small>
</app-settings-entry>
</ng-container>
<button class="btn btn-success float-right"

View File

@ -33,14 +33,14 @@ export class MapSettingsComponent extends SettingsComponent<ClientConfig.MapConf
addNewLayer() {
this.settings.customLayers.push({
name: 'Layer-' + this.settings.customLayers.length,
this.states.customLayers.value.push({
name: 'Layer-' + this.states.customLayers.value.length,
url: ''
});
}
removeLayer(layer: ClientConfig.MapLayers) {
this.settings.customLayers.splice(this.settings.customLayers.indexOf(layer), 1);
this.states.customLayers.value.splice(this.states.customLayers.value.indexOf(layer), 1);
}
}

View File

@ -6,14 +6,16 @@
<bSwitch
class="switch"
name="enabled"
[switch-on-color]="'success'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
switch-on-color="success"
switch-inverse="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
[switch-disabled]="inProgress"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.enabled">
switch-handle-width="100"
switch-label-width="20"
[(ngModel)]="states.enabled.value">
</bSwitch>
</div>
</h5>

View File

@ -8,161 +8,89 @@
<p class="title" i18n>Threads:</p>
<div class="form-group row" >
<label class="col-md-2 control-label" for="enableThreading" i18n>Threading</label>
<div class="col-md-10">
<bSwitch
id="enableThreading"
class="switch"
name="enable"
[switch-on-color]="'primary'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.Server.enabled">
</bSwitch>
<small class="form-text text-muted" i18n>Runs directory scanning and thumbnail generation (only for Jimp) in a
different thread
</small>
</div>
</div>
<div class="form-group row" [hidden]="simplifiedMode || settings.Server.enabled == false">
<label class="col-md-2 control-label" for="thumbnailThreads" i18n>Thumbnail threads</label>
<div class="col-md-10">
<select id="thumbnailThreads" class="form-control" [(ngModel)]="settings.Server.thumbnailThreads"
name="Server[thumbnailThreads]" required>
<option [ngValue]="0">auto</option>
<option *ngFor="let i of threads" [ngValue]="i">{{i}}</option>
</select>
<small class="form-text text-muted" i18n>Number of threads that are used to generate thumbnails. If auto, number of CPU cores -1 threads will be used.</small>
</div>
</div>
<app-settings-entry
name="Threading"
description="Runs directory scanning and thumbnail generation (only for Jimp) in a different thread."
i18n-description i18n-name
[ngModel]="states.Server.enabled"
required="true">
</app-settings-entry>
<app-settings-entry
name="Thumbnail threads"
description="Number of threads that are used to generate thumbnails. If auto, number of CPU cores -1 threads will be used."
i18n-description i18n-name
[ngModel]="states.Server.thumbnailThreads"
[options]="threads"
[simplifiedMode]="simplifiedMode || states.Server.enabled.value == false"
required="true">
</app-settings-entry>
<hr/>
<p class="title" i18n>Misc:</p>
<div class="form-group row">
<label class="col-md-2 control-label" for="enableOnScrollThumbnailPrioritising" i18n>Scroll based thumbnail
generation</label>
<div class="col-md-10">
<bSwitch
id="enableOnScrollThumbnailPrioritising"
class="switch"
name="enableOnScrollThumbnailPrioritising"
[switch-on-color]="'primary'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.Client.enableOnScrollThumbnailPrioritising">
</bSwitch>
<small class="form-text text-muted" i18n>Those thumbnails get higher priority that are visible on the screen
</small>
</div>
</div>
<div class="form-group row">
<label class="col-md-2 control-label" for="enableOnScrollRendering" i18n>Lazy image rendering</label>
<div class="col-md-10">
<bSwitch
id="enableOnScrollRendering"
class="switch"
name="enableOnScrollRendering"
[switch-on-color]="'primary'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.Client.enableOnScrollRendering">
</bSwitch>
<small class="form-text text-muted" i18n>Shows only the required amount of photos at once. Renders more if
page bottom is reached
</small>
</div>
</div>
<app-settings-entry
name="Scroll based thumbnail generation"
description="Those thumbnails get higher priority that are visible on the screen."
i18n-description i18n-name
[ngModel]="states.Client.enableOnScrollThumbnailPrioritising"
required="true">
</app-settings-entry>
<app-settings-entry
name="Lazy image rendering"
description="Shows only the required amount of photos at once. Renders more if page bottom is reached."
i18n-description i18n-name
[ngModel]="states.Client.enableOnScrollRendering"
required="true">
</app-settings-entry>
<div class="form-group row">
<label class="col-md-2 control-label" for="enableCache" i18n>Cache</label>
<div class="col-md-10">
<bSwitch
id="enableCache"
class="switch"
name="enableCache"
[switch-on-color]="'primary'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.Client.enableCache">
</bSwitch>
<small class="form-text text-muted" i18n>Caches directory contents and search results for better performance
</small>
</div>
</div>
<app-settings-entry
name="Cache"
description="Caches directory contents and search results for better performance."
i18n-description i18n-name
[ngModel]="states.Client.enableCache"
required="true">
</app-settings-entry>
<app-settings-entry
name="Caption first naming"
description="Show the caption (IPTC 120) tags from the EXIF data instead of the filenames."
i18n-description i18n-name
[ngModel]="states.Client.captionFirstNaming"
required="true">
</app-settings-entry>
<div class="form-group row">
<label class="col-md-2 control-label" for="enableCache" i18n>Caption first naming</label>
<div class="col-md-10">
<bSwitch
id="captionFirstNaming"
class="switch"
name="captionFirstNaming"
[switch-on-color]="'primary'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.Client.captionFirstNaming">
</bSwitch>
<small class="form-text text-muted" i18n>Show the caption (IPTC 120) tags from the EXIF data instead of the filenames.
</small>
</div>
</div>
<hr/>
<p class="title" i18n>Navigation bar:</p>
<div class="form-group row">
<label class="col-md-2 control-label" for="showItemCount" [hidden]="simplifiedMode" i18n>Show item count</label>
<div class="col-md-10">
<bSwitch
id="showItemCount"
class="switch"
name="showItemCount"
[switch-on-color]="'primary'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.Client.NavBar.showItemCount">
</bSwitch>
<small class="form-text text-muted" i18n>Show the number of items (photos) in the folder
</small>
</div>
</div>
<app-settings-entry
name="Show item count"
description="Show the number of items (photos) in the folder."
i18n-description i18n-name
[ngModel]="states.Client.NavBar.showItemCount"
[simplifiedMode]="simplifiedMode"
required="true">
</app-settings-entry>
<div class="form-group row" [hidden]="simplifiedMode">
<label class="col-md-2 control-label" for="defaultPhotoSortingMethod" i18n>Default photo sorting method</label>
<div class="col-md-10">
<select id="defaultPhotoSortingMethod" class="form-control" [(ngModel)]="settings.Client.defaultPhotoSortingMethod"
name="defaultPhotoSortingMethod" required>
<option *ngFor="let type of types" [ngValue]="type.key">{{type.key | stringifySorting}}
</option>
</select>
</div>
</div>
<app-settings-entry
name="Default photo sorting method"
i18n-name
[ngModel]="states.Client.defaultPhotoSortingMethod"
[optionMap]="sortingMap"
[simplifiedMode]="simplifiedMode"
required="true">
</app-settings-entry>
<button class="btn btn-success float-right"

View File

@ -8,6 +8,7 @@ import {OtherConfigDTO} from '../../../../../common/entities/settings/OtherConfi
import {I18n} from '@ngx-translate/i18n-polyfill';
import {Utils} from '../../../../../common/Utils';
import {SortingMethods} from '../../../../../common/entities/SortingMethods';
import {StringifySortingMethod} from '../../../pipes/StringifySortingMethod';
@Component({
selector: 'app-settings-other',
@ -20,17 +21,24 @@ export class OtherSettingsComponent extends SettingsComponent<OtherConfigDTO> im
types: { key: number; value: string }[] = [];
threads: number[] = Utils.createRange(1, 100);
threads: { key: number, value: string }[] = [{key: 0, value: 'auto'}].concat(Utils.createRange(1, 100)
.map(v => ({key: v, value: '' + v})));
sortingMap: any;
constructor(_authService: AuthenticationService,
_navigation: NavigationService,
_settingsService: OtherSettingsService,
notification: NotificationService,
i18n: I18n) {
i18n: I18n,
private formatter: StringifySortingMethod) {
super(i18n('Other'), _authService, _navigation, _settingsService, notification, i18n, s => ({
Server: s.Server.Threading,
Client: s.Client.Other
}));
this.sortingMap = (v: { key: number; value: string }) => {
v.value = this.formatter.transform(v.key);
return v;
};
this.types = Utils.enumToArray(SortingMethods);
this.hasAvailableSettings = !this.simplifiedMode;
}

View File

@ -4,90 +4,64 @@
{{Name}}</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<div [hidden]="settings.photoProcessingLibrary!=PhotoProcessingLib.Jimp"
<div [hidden]="states.photoProcessingLibrary.value!=PhotoProcessingLib.Jimp"
class="alert alert-warning"
role="alert" i18n>It is highly recommended to use hardware accelerated (sharp or gm) lib for thumbnail
generation
</div>
<div [hidden]="simplifiedMode">
<div class="form-group row">
<label class="col-md-2 control-label" for="lib" i18n>Thumbnail generation library</label>
<div class="col-md-10">
<select id="lib" class="form-control" [(ngModel)]="settings.photoProcessingLibrary"
name="type" required>
<option *ngFor="let type of libTypes" [ngValue]="type.key">{{type.value}}
</option>
</select>
<small *ngIf="settings.photoProcessingLibrary==PhotoProcessingLib.sharp"
class="form-text text-muted" i18n>Make sure that sharp node module is installed (npm install sharp).
</small>
<small *ngIf="settings.photoProcessingLibrary==PhotoProcessingLib.gm"
class="form-text text-muted">
<ng-container i18n>Make sure that gm node module and</ng-container>
<a href="http://www.graphicsmagick.org/">GraphicsMagick</a>
<ng-container i18n>are installed (npm install sharp).</ng-container>
</small>
</div>
</div>
<app-settings-entry
name="Thumbnail generation library"
i18n-name
[ngModel]="states.photoProcessingLibrary"
[optionMap]="libTypesMap"
[simplifiedMode]="simplifiedMode"
required="true">
<small *ngIf="states.photoProcessingLibrary.value==PhotoProcessingLib.sharp"
class="form-text text-muted" i18n>Make sure that sharp node module is installed (npm install sharp).
</small>
<small *ngIf="states.photoProcessingLibrary.value==PhotoProcessingLib.gm"
class="form-text text-muted">
<ng-container i18n>Make sure that gm node module and</ng-container>
<a href="http://www.graphicsmagick.org/">GraphicsMagick</a>
<ng-container i18n>are installed (npm install sharp).</ng-container>
</small>
</app-settings-entry>
<hr/>
</div>
<p class="title" i18n>Photo converting:</p>
<div class="form-group row">
<label class="col-md-2 control-label" for="enablePhotoConverting" i18n>Converting</label>
<div class="col-md-10">
<bSwitch
id="enablePhotoConverting"
class="switch"
name="enablePhotoConverting"
[switch-on-color]="'primary'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.client.Converting.enabled">
</bSwitch>
<small class="form-text text-muted" i18n>Downsizes photos for faster preview loading. (Zooming in to the photo
loads the original)</small>
</div>
</div>
<app-settings-entry
name="Converting"
description="Downsizes photos for faster preview loading. (Zooming in to the photo loads the original)."
i18n-description i18n-name
[ngModel]="states.client.Converting.enabled"
required="true">
</app-settings-entry>
<div class="form-group row" [hidden]="!settings.client.Converting.enabled || simplifiedMode">
<label class="col-md-2 control-label" for="onTheFlyConverting" i18n>On the fly converting </label>
<div class="col-md-10">
<bSwitch
id="onTheFlyConverting"
class="switch"
name="onTheFlyConverting"
[switch-on-color]="'primary'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.server.Converting.onTheFly">
</bSwitch>
<small class="form-text text-muted" i18n>Converts photos on the fly, when they are requested.</small>
</div>
</div>
<app-settings-entry
name="On the fly converting"
description="Converts photos on the fly, when they are requested."
i18n-description i18n-name
[ngModel]="states.server.Converting.onTheFly"
[simplifiedMode]="simplifiedMode"
[disabled]="!states.client.Converting.enabled.value"
required="true">
</app-settings-entry>
<div class="form-group row" [hidden]="!settings.client.Converting.enabled">
<label class="col-md-2 control-label" for="resolution" i18n>Resolution</label>
<div class="col-md-10">
<select id="resolution" class="form-control" [(ngModel)]="settings.server.Converting.resolution"
name="resolution" required>
<option *ngFor="let resolution of resolutions" [ngValue]="resolution">{{resolution}}px
</option>
</select>
<small class="form-text text-muted" i18n>The shorter edge of the converted photo will be scaled down to this,
while
keeping the aspect ratio.</small>
</div>
</div>
<app-settings-entry
name="Resolution"
description="The shorter edge of the converted photo will be scaled down to this, while keeping the aspect ratio."
i18n-description i18n-name
[ngModel]="states.server.Converting.resolution"
[options]="resolutions"
[disabled]="!states.client.Converting.enabled.value"
required="true">
</app-settings-entry>
<button class="btn btn-success float-right"
@ -99,7 +73,7 @@
(click)="reset()" i18n>Reset
</button>
<div [hidden]="!settings.client.Converting.enabled">
<div [hidden]="!states.client.Converting.enabled.value">
<app-settings-job-button class="mt-2 mt-md-0 float-left"
[soloRun]="true"
(error)="error=$event"

View File

@ -6,7 +6,6 @@ import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {I18n} from '@ngx-translate/i18n-polyfill';
import {ScheduledJobsService} from '../scheduled-jobs.service';
import {Utils} from '../../../../../common/Utils';
import {DefaultsJobs, JobDTO} from '../../../../../common/entities/job/JobDTO';
import {JobProgressStates} from '../../../../../common/entities/job/JobProgressDTO';
import {ServerConfig} from '../../../../../common/config/private/PrivateConfig';
@ -25,19 +24,11 @@ export class PhotoSettingsComponent extends SettingsComponent<{
server: ServerConfig.PhotoConfig,
client: ClientConfig.PhotoConfig
}> {
resolutions = [720, 1080, 1440, 2160, 4320];
readonly resolutionTypes = [720, 1080, 1440, 2160, 4320];
resolutions: { key: number, value: string }[] = [];
PhotoProcessingLib = ServerConfig.PhotoProcessingLib;
JobProgressStates = JobProgressStates;
libTypes = Utils
.enumToArray(ServerConfig.PhotoProcessingLib).map((v) => {
if (v.value.toLowerCase() === 'sharp') {
v.value += ' ' + this.i18n('(recommended)');
} else {
v.value += ' ' + this.i18n('(deprecated, will be removed)');
}
return v;
});
readonly jobName = DefaultsJobs[DefaultsJobs['Photo Converting']];
constructor(_authService: AuthenticationService,
@ -52,15 +43,24 @@ export class PhotoSettingsComponent extends SettingsComponent<{
server: s.Server.Media.Photo
}));
const currentRes = _settingsService.Settings.value.Server.Media.Photo.Converting.resolution;
if (this.resolutions.indexOf(currentRes) === -1) {
this.resolutions.push(currentRes);
if (this.resolutionTypes.indexOf(currentRes) === -1) {
this.resolutionTypes.push(currentRes);
}
this.resolutions = this.resolutionTypes.map(e => ({key: e, value: e + 'px'}));
}
get Progress() {
return this.jobsService.progress.value[JobDTO.getHashName(DefaultsJobs[DefaultsJobs['Photo Converting']])];
}
libTypesMap = (v: { key: number, value: string }) => {
if (v.value.toLowerCase() === 'sharp') {
v.value += ' ' + this.i18n('(recommended)');
} else {
v.value += ' ' + this.i18n('(deprecated, will be removed)');
}
return v;
};
}

View File

@ -1,27 +1,29 @@
<form #settingsForm="ngForm">
<div class="card mb-4"
[ngClass]="settings.enabled && !_settingsService.isSupported()?'panel-warning':''">
[ngClass]="states.enabled.value && !_settingsService.isSupported()?'panel-warning':''">
<h5 class="card-header">
{{Name}}
<div class="switch-wrapper">
<bSwitch
class="switch"
name="enabled"
[switch-on-color]="'success'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-disabled]="inProgress || (!settings.enabled && !_settingsService.isSupported())"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.enabled">
switch-on-color="success"
switch-inverse="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
[switch-disabled]="inProgress || !_settingsService.isSupported()"
switch-handle-width="100"
switch-label-width="20"
[(ngModel)]="states.enabled.value">
</bSwitch>
</div>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<ng-container *ngIf="settings.enabled || _settingsService.isSupported()">
<ng-container *ngIf="states.enabled.value || _settingsService.isSupported()">
<div class="alert alert-secondary" role="alert" i18n>
This feature enables you to generate 'random photo' urls.
@ -30,7 +32,7 @@
</div>
</ng-container>
<div class="panel-info" *ngIf="(!settings.enabled && !_settingsService.isSupported())" i18n>
<div class="panel-info" *ngIf="(!states.enabled.value && !_settingsService.isSupported())" i18n>
Random Photo is not supported with these settings
</div>
<button class="btn btn-success float-right"

View File

@ -1,71 +1,53 @@
<form #settingsForm="ngForm">
<div class="card mb-4"
[ngClass]="settings.enabled && !_settingsService.isSupported()?'panel-warning':''">
[ngClass]="states.enabled.value && !_settingsService.isSupported()?'panel-warning':''">
<h5 class="card-header">
{{Name}}
<div class="switch-wrapper">
<bSwitch
class="switch"
name="enabled"
[switch-on-color]="'success'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-disabled]="inProgress || (!settings.enabled && !_settingsService.isSupported())"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.enabled">
switch-on-color="success"
switch-inverse="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
[switch-disabled]="inProgress || !_settingsService.isSupported()"
switch-handle-width="100"
switch-label-width="20"
[(ngModel)]="states.enabled.value">
</bSwitch>
</div>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<ng-container *ngIf="settings.enabled || _settingsService.isSupported()">
<div class="form-group row">
<label class="col-md-2 control-label" for="autocompleteEnabled" i18n>Autocomplete</label>
<div class="col-md-10">
<bSwitch
id="autocompleteEnabled"
class="switch"
name="autocompleteEnabled"
[switch-on-color]="'primary'"
[switch-disabled]="!settings.enabled"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.AutoComplete.enabled">
</bSwitch>
<small class="form-text text-muted" i18n>Show hints while typing search query</small>
</div>
</div>
<ng-container *ngIf="states.enabled.value || _settingsService.isSupported()">
<app-settings-entry
name="Autocomplete"
description="Show hints while typing search query."
i18n-description i18n-name
[ngModel]="states.AutoComplete.enabled"
required="true">
</app-settings-entry>
<app-settings-entry
name="Instant search"
description="Enables showing search results, while typing search query."
i18n-description i18n-name
[ngModel]="states.instantSearchEnabled"
required="true">
</app-settings-entry>
<div class="form-group row">
<label class="col-md-2 control-label" for="instantSearchEnabled" i18n>Instant search</label>
<div class="col-md-10">
<bSwitch
id="instantSearchEnabled"
class="switch"
name="instantSearchEnabled"
[switch-on-color]="'primary'"
[switch-disabled]="!settings.enabled"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.instantSearchEnabled">
</bSwitch>
<small class="form-text text-muted" i18n>Enables showing search results, while typing search query</small>
</div>
</div>
</ng-container>
<div class="panel-info" *ngIf="(!settings.enabled && !_settingsService.isSupported())" i18n>
<div class="panel-info" *ngIf="(!states.enabled.value && !_settingsService.isSupported())" i18n>
Search is not supported with these settings
</div>
<button class="btn btn-success float-right"

View File

@ -1,50 +1,41 @@
<form #settingsForm="ngForm">
<div class="card mb-4"
[ngClass]="settings.enabled && !_settingsService.isSupported()?'panel-warning':''">
[ngClass]="states.enabled.value && !_settingsService.isSupported()?'panel-warning':''">
<h5 class="card-header">
{{Name}}
<div class="switch-wrapper">
<bSwitch
class="switch"
name="enabled"
[switch-on-color]="'success'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-disabled]="inProgress || (!settings.enabled && !_settingsService.isSupported())"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.enabled">
switch-on-color="success"
switch-inverse="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
[switch-disabled]="inProgress || !_settingsService.isSupported()"
switch-handle-width="100"
switch-label-width="20"
[(ngModel)]="states.enabled.value">
</bSwitch>
</div>
</h5>
<div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<ng-container *ngIf="settings.enabled || _settingsService.isSupported()">
<div class="form-group row">
<label class="col-md-2 control-label" for="passwordProtected" i18n>Password protected</label>
<div class="col-md-10">
<bSwitch
id="passwordProtected"
class="switch"
name="passwordProtected"
[switch-on-color]="'primary'"
[switch-disabled]="!settings.enabled"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.passwordProtected">
</bSwitch>
<small class="form-text text-muted" i18n>Enables password protected sharing links</small>
<ng-container *ngIf="states.enabled.value || _settingsService.isSupported()">
<app-settings-entry
name="Password protected"
description="Enables password protected sharing links."
i18n-description i18n-name
[ngModel]="states.passwordProtected"
required="true">
</app-settings-entry>
</div>
</div>
</ng-container>
<div class="panel-info" *ngIf="(!settings.enabled && !_settingsService.isSupported())" i18n>
<div class="panel-info" *ngIf="(!states.enabled.value && !_settingsService.isSupported())" i18n>
Sharing is not supported with these settings
</div>
<button class="btn btn-success float-right"

View File

@ -7,62 +7,48 @@
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<div class="form-group row" [hidden]="simplifiedMode">
<label class="col-md-2 control-label" for="quality" i18n>Thumbnail Quality</label>
<div class="col-md-10">
<bSwitch
id="quality"
class="switch"
name="enabled"
[switch-on-color]="'primary'"
[switch-inverse]="true"
[switch-off-text]="text.Low"
[switch-on-text]="text.High"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.server.qualityPriority">
</bSwitch>
<small class="form-text text-muted" i18n>High quality may be slow. Especially with Jimp.</small>
<app-settings-entry
name="Thumbnail Quality"
description="High quality may be slow. Especially with Jimp."
i18n-description i18n-name
[ngModel]="states.server.qualityPriority"
[simplifiedMode]="simplifiedMode"
required="true">
</app-settings-entry>
</div>
</div>
<div class="form-group row" [hidden]="simplifiedMode">
<label class="col-md-2 control-label" for="icon" i18n>Icon size</label>
<div class="col-md-10">
<input type="number" class="form-control" placeholder="30"
id="icon"
[(ngModel)]="settings.client.iconSize"
min="1"
max="100"
step="1"
name="icon" required>
<small class="form-text text-muted" i18n>Icon size (used on maps)</small>
<app-settings-entry
name="Icon size"
description="Icon size (used on maps)."
i18n-description i18n-name
[ngModel]="states.client.iconSize"
[simplifiedMode]="simplifiedMode"
required="true">
</app-settings-entry>
<app-settings-entry
name="Thumbnail sizes"
i18n-name
placeholder="240; 480"
[ngModel]="states.client.thumbnailSizes"
[simplifiedMode]="simplifiedMode"
required="true">
<small class="form-text text-muted">
<ng-container i18n>Size of the thumbnails.</ng-container>
<br/>
<ng-container i18n>The best matching size will be generated. (More sizes give better quality, but use more
storage and CPU to render.)
</ng-container>
<br/>
<ng-container i18n>';' separated integers. If size is 240, that shorter side of the thumbnail will have 160
pixels.
</ng-container>
</small>
</app-settings-entry>
</div>
</div>
<div class="form-group row" [hidden]="simplifiedMode">
<label class="col-md-2 control-label" for="thumbnailSizes" i18n>Thumbnail sizes</label>
<div class="col-md-10">
<input type="text" class="form-control" placeholder="240; 480"
id="thumbnailSizes"
[(ngModel)]="ThumbnailSizes"
name="thumbnailSizes" required>
<small class="form-text text-muted">
<ng-container i18n>Size of the thumbnails.</ng-container>
<br/>
<ng-container i18n>The best matching size will be generated. (More sizes give better quality, but use more
storage and CPU to render.)
</ng-container>
<br/>
<ng-container i18n>';' separated integers. If size is 240, that shorter side of the thumbnail will have 160
pixels.
</ng-container>
</small>
</div>
</div>
<button class="btn btn-success float-right"
[disabled]="!settingsForm.form.valid || !changed || inProgress"

View File

@ -37,20 +37,9 @@ export class ThumbnailSettingsComponent
}
get Config(): any {
return {sizes: this.original.client.thumbnailSizes[0]};
return {sizes: this.states.client.thumbnailSizes.original[0]};
}
get ThumbnailSizes(): string {
return this.settings.client.thumbnailSizes.join('; ');
}
set ThumbnailSizes(value: string) {
value = value.replace(new RegExp(',', 'g'), ';');
value = value.replace(new RegExp(' ', 'g'), ';');
this.settings.client.thumbnailSizes = value.split(';')
.map(s => parseInt(s, 10))
.filter(i => !isNaN(i) && i > 0);
}
get Progress() {
return this.jobsService.progress.value[JobDTO.getHashName(this.jobName, this.Config)];

View File

@ -5,12 +5,12 @@
<bSwitch
class="switch"
name="enabled"
[switch-on-color]="'success'"
[switch-inverse]="true"
switch-on-color="success"
switch-inverse="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
[switch-handle-width]="100"
[switch-label-width]="20"
switch-handle-width="100"
switch-label-width="20"
[switch-disabled]="inProgress"
[(ngModel)]="enabled"
(changeState)="switched($event)">

View File

@ -7,14 +7,16 @@
<bSwitch
class="switch"
name="enabled"
[switch-on-color]="'success'"
[switch-inverse]="true"
[switch-off-text]="text.Disabled"
[switch-on-text]="text.Enabled"
switch-on-color="success"
switch-inverse="true"
switch-off-text="Disabled"
switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
[switch-disabled]="inProgress"
[switch-handle-width]="100"
[switch-label-width]="20"
[(ngModel)]="settings.client.enabled">
switch-handle-width="100"
switch-label-width="20"
[(ngModel)]="states.client.enabled.value">
</bSwitch>
</div>
</h5>
@ -42,58 +44,50 @@
</ng-container>&nbsp;
</div>
<div class="form-group row" [hidden]="simplifiedMode">
<label class="col-md-2 control-label" for="format" i18n>Format</label>
<div class="col-md-10">
<select id="format" class="form-control" [(ngModel)]="settings.server.transcoding.format"
(ngModelChange)="formatChanged($event)"
name="format" required>
<option *ngFor="let format of formats" [ngValue]="format">{{format}}
</option>
</select>
</div>
</div>
<app-settings-entry
name="Format"
i18n-name
[ngModel]="states.server.transcoding.format"
[simplifiedMode]="simplifiedMode"
(ngModelChange)="formatChanged($event)"
[options]="formats"
required="true">
</app-settings-entry>
<div class="form-group row" [hidden]="simplifiedMode">
<label class="col-md-2 control-label" for="codec" i18n>Codec</label>
<div class="col-md-10">
<select id="codec" class="form-control" [(ngModel)]="settings.server.transcoding.codec"
name="codec" required>
<option *ngFor="let codec of codecs[settings.server.transcoding.format]" [ngValue]="codec">{{codec}}
</option>
</select>
</div>
</div>
<app-settings-entry
name="Codec"
i18n-name
[ngModel]="states.server.transcoding.codec"
[simplifiedMode]="simplifiedMode"
[options]="codecs[states.server.transcoding.format.value]"
required="true">
</app-settings-entry>
<div class="form-group row">
<label class="col-md-2 control-label" for="resolution" i18n>Resolution</label>
<div class="col-md-10">
<select id="resolution" class="form-control" [(ngModel)]="settings.server.transcoding.resolution"
(ngModelChange)="updateBitRate()"
name="resolution" required>
<option *ngFor="let resolution of resolutions" [ngValue]="resolution">{{resolution}}p
</option>
</select>
<small class="form-text text-muted" i18n>The height of the output video will be scaled down to this, while
keeping the aspect ratio.</small>
</div>
</div>
<app-settings-entry
name="Resolution"
description="The height of the output video will be scaled down to this, while keeping the aspect ratio."
i18n-name i18n-description
[ngModel]="states.server.transcoding.resolution"
(ngModelChange)="updateBitRate()"
[options]="resolutions"
required="true">
</app-settings-entry>
<div class="form-group row" [hidden]="simplifiedMode">
<label class="col-md-2 control-label" for="fps" i18n>FPS</label>
<div class="col-md-10">
<select id="fps" class="form-control" [(ngModel)]="settings.server.transcoding.fps"
(ngModelChange)="updateBitRate()"
name="fps" required>
<option *ngFor="let fps of fps" [ngValue]="fps">{{fps}}
</option>
</select>
<small class="form-text text-muted" i18n>Target frame per second (fps) of the output video will be scaled down
this this.</small>
</div>
</div>
<div class="form-group row">
<app-settings-entry
name="FPS"
description="Target frame per second (fps) of the output video will be scaled down this this."
i18n-name i18n-description
[ngModel]="states.server.transcoding.fps"
(ngModelChange)="updateBitRate()"
[options]="fps"
[simplifiedMode]="simplifiedMode"
required="true">
</app-settings-entry>
<div class="form-group row"
[class.changed-settings]="states.server.transcoding.bitRate.value !== states.server.transcoding.bitRate.default">
<label class="col-md-2 control-label" for="bitRate" i18n>Bit rate</label>
<div class="col-md-10">
<div class="input-group">
@ -115,9 +109,6 @@
</div>
<button class="btn btn-success float-right"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save

View File

@ -21,10 +21,16 @@ import {ClientConfig} from '../../../../../common/config/public/ClientConfig';
})
export class VideoSettingsComponent extends SettingsComponent<{ server: ServerConfig.VideoConfig, client: ClientConfig.VideoConfig }> {
resolutions: ServerConfig.resolutionType[] = [360, 480, 720, 1080, 1440, 2160, 4320];
codecs: { [key: string]: ServerConfig.codecType[] } = {webm: ['libvpx', 'libvpx-vp9'], mp4: ['libx264', 'libx265']};
formats: ServerConfig.formatType[] = ['mp4', 'webm'];
fps = [24, 25, 30, 48, 50, 60];
readonly resolutionTypes: ServerConfig.resolutionType[] = [360, 480, 720, 1080, 1440, 2160, 4320];
resolutions: { key: number, value: string }[] = [];
codecs: { [key: string]: { key: ServerConfig.codecType, value: ServerConfig.codecType }[] } = {
webm: ['libvpx', 'libvpx-vp9'].map((e: ServerConfig.codecType) => ({key: e, value: e})),
mp4: ['libx264', 'libx265'].map((e: ServerConfig.codecType) => ({key: e, value: e}))
};
formats: { key: ServerConfig.formatType, value: ServerConfig.formatType }[] = ['mp4', 'webm']
.map((e: ServerConfig.formatType) => ({key: e, value: e}));
fps = [24, 25, 30, 48, 50, 60].map(e => ({key: e, value: e}));
JobProgressStates = JobProgressStates;
readonly jobName = DefaultsJobs[DefaultsJobs['Video Converting']];
@ -41,9 +47,10 @@ export class VideoSettingsComponent extends SettingsComponent<{ server: ServerCo
}));
const currentRes = _settingsService.Settings.value.Server.Media.Video.transcoding.resolution;
if (this.resolutions.indexOf(currentRes) === -1) {
this.resolutions.push(currentRes);
if (this.resolutionTypes.indexOf(currentRes) === -1) {
this.resolutionTypes.push(currentRes);
}
this.resolutions = this.resolutionTypes.map(e => ({key: e, value: e + 'px'}));
}
@ -52,11 +59,11 @@ export class VideoSettingsComponent extends SettingsComponent<{ server: ServerCo
}
get bitRate(): number {
return this.settings.server.transcoding.bitRate / 1024 / 1024;
return this.states.server.transcoding.bitRate.value / 1024 / 1024;
}
set bitRate(value: number) {
this.settings.server.transcoding.bitRate = Math.round(value * 1024 * 1024);
this.states.server.transcoding.bitRate.value = Math.round(value * 1024 * 1024);
}
getRecommendedBitRate(resolution: number, fps: number) {
@ -83,12 +90,12 @@ export class VideoSettingsComponent extends SettingsComponent<{ server: ServerCo
}
updateBitRate() {
this.settings.server.transcoding.bitRate = this.getRecommendedBitRate(this.settings.server.transcoding.resolution,
this.settings.server.transcoding.fps);
this.states.server.transcoding.bitRate.value = this.getRecommendedBitRate(this.states.server.transcoding.resolution.value,
this.states.server.transcoding.fps.value);
}
formatChanged(format: ServerConfig.formatType) {
this.settings.server.transcoding.codec = this.codecs[format][0];
this.states.server.transcoding.codec.value = this.codecs[format][0].key;
}

View File

@ -12,6 +12,10 @@
margin-left: -4px;
}
.changed-settings .bootstrap-switch{
border-color: #007bff;
border-width: 1px;
}
#toast-container > div {
opacity:1;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -42,7 +42,7 @@ describe('SettingsRouter', () => {
result.res.should.have.status(200);
result.body.should.be.a('object');
should.equal(result.body.error, null);
result.body.result.should.deep.equal(JSON.parse(JSON.stringify(originalSettings.toJSON({attachDefaults: true}))));
result.body.result.should.deep.equal(JSON.parse(JSON.stringify(originalSettings.toJSON({attachState: true, attachVolatile: true}))));
});
});