1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-11-24 08:42:24 +02:00

implementing advanced settings

This commit is contained in:
Patrik J. Braun 2020-02-04 19:37:47 +01:00
parent 73fee438ec
commit d152900e21
47 changed files with 3248 additions and 2819 deletions

8
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "pigallery2", "name": "pigallery2",
"version": "1.8.0", "version": "1.8.1",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -18721,9 +18721,9 @@
} }
}, },
"typeconfig": { "typeconfig": {
"version": "2.0.0", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/typeconfig/-/typeconfig-2.0.0.tgz", "resolved": "https://registry.npmjs.org/typeconfig/-/typeconfig-2.0.6.tgz",
"integrity": "sha512-IwZH+P8J4qhyrOfKzJZJx6raEkaBjjZIiE+rzrWgdOhqVhO5Cv+pkOQdNo+z/Wq5wER5YWeDNX7Wdbqv0jt6IA==", "integrity": "sha512-OnXPXSDaK1mzH6dJ1HB9Q70ruJYngEhemwL9Y8+nG5E40Je4MMODuEY+tfjMtIiFIN772DU5Q+NebulLZNDjpw==",
"requires": { "requires": {
"optimist": "0.6.1" "optimist": "0.6.1"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "pigallery2", "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)", "description": "This is a photo gallery optimised for running low resource servers (especially on raspberry pi)",
"author": "Patrik J. Braun", "author": "Patrik J. Braun",
"homepage": "https://github.com/bpatrik/PiGallery2", "homepage": "https://github.com/bpatrik/PiGallery2",
@ -16,6 +16,7 @@
"coverage": "nyc report --reporter=text-lcov | coveralls", "coverage": "nyc report --reporter=text-lcov | coveralls",
"start": "node ./src/backend/index", "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": "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", "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", "merge-new-translation": "gulp merge-new-translation",
"add-translation": "gulp add-translation", "add-translation": "gulp add-translation",
@ -47,7 +48,7 @@
"sqlite3": "4.1.1", "sqlite3": "4.1.1",
"ts-exif-parser": "0.1.4", "ts-exif-parser": "0.1.4",
"ts-node-iptc": "1.0.11", "ts-node-iptc": "1.0.11",
"typeconfig": "2.0.0", "typeconfig": "2.0.6",
"typeorm": "0.2.21", "typeorm": "0.2.21",
"winston": "2.4.4" "winston": "2.4.4"
}, },

View File

@ -63,7 +63,10 @@ export class RenderingMWs {
public static async renderConfig(req: Request, res: Response, next: NextFunction) { public static async renderConfig(req: Request, res: Response, next: NextFunction) {
const originalConf = await Config.original(); const originalConf = await Config.original();
originalConf.Server.sessionSecret = null; 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); res.json(message);
} }

View File

@ -82,7 +82,7 @@ export class PublicRouter {
res.tpl.user.csrfToken = req.csrfToken(); res.tpl.user.csrfToken = req.csrfToken();
} }
} }
res.tpl.clientConfig = {Client: Config.Client}; res.tpl.clientConfig = {Client: Config.Client.toJSON({attachVolatile: true})};
return next(); return next();
}); });

View File

@ -61,6 +61,10 @@ export class Utils {
if (!object) { if (!object) {
return false; return false;
} }
if (Array.isArray(object) && object.length !== filter.length) {
return false;
}
const keys = Object.keys(filter); const keys = Object.keys(filter);
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
const key = keys[i]; const key = keys[i];
@ -158,10 +162,7 @@ export class Utils {
}); });
} }
public static enumToArray(EnumType: any): Array<{ public static enumToArray(EnumType: any): { key: number; value: string }[] {
key: number;
value: string;
}> {
const arr: Array<{ key: number; value: string; }> = []; const arr: Array<{ key: number; value: string; }> = [];
for (const enumMember in EnumType) { for (const enumMember in EnumType) {
if (!EnumType.hasOwnProperty(enumMember)) { if (!EnumType.hasOwnProperty(enumMember)) {

View File

@ -2,10 +2,8 @@ import {IPrivateConfig, ServerConfig} from './PrivateConfig';
import {ClientConfig} from '../public/ClientConfig'; import {ClientConfig} from '../public/ClientConfig';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import * as path from 'path'; import * as path from 'path';
import {ConfigClass} from 'typeconfig/src/decorators/class/ConfigClass'; import {ConfigClass, ConfigClassBuilder} from 'typeconfig/node';
import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty'; import {ConfigProperty, IConfigClass} from 'typeconfig/common';
import {IConfigClass} from 'typeconfig/src/decorators/class/IConfigClass';
import {ConfigClassBuilder} from 'typeconfig/src/decorators/builders/ConfigClassBuilder';
@ConfigClass({ @ConfigClass({
@ -16,7 +14,7 @@ import {ConfigClassBuilder} from 'typeconfig/src/decorators/builders/ConfigClass
cli: { cli: {
enable: { enable: {
configPath: true, configPath: true,
attachDefaults: true, attachState: true,
attachDescription: true, attachDescription: true,
rewriteCLIConfig: true, rewriteCLIConfig: true,
rewriteENVConfig: true, rewriteENVConfig: true,
@ -33,7 +31,7 @@ export class PrivateConfigClass implements IPrivateConfig {
@ConfigProperty() @ConfigProperty()
Server: ServerConfig.Config = new ServerConfig.Config(); Server: ServerConfig.Config = new ServerConfig.Config();
@ConfigProperty() @ConfigProperty()
Client: ClientConfig.Config = new ClientConfig.Config(); Client: IConfigClass & ClientConfig.Config = <IConfigClass & ClientConfig.Config>(new ClientConfig.Config());
constructor() { constructor() {
if (!this.Server.sessionSecret || this.Server.sessionSecret.length === 0) { 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 {DefaultsJobs} from '../../entities/job/JobDTO';
import {JobScheduleDTO, JobTrigger, JobTriggerType} from '../../entities/job/JobScheduleDTO'; import {JobScheduleDTO, JobTrigger, JobTriggerType} from '../../entities/job/JobScheduleDTO';
import {ClientConfig} from '../public/ClientConfig'; import {ClientConfig} from '../public/ClientConfig';
import { SubConfigClass } from 'typeconfig/src/decorators/class/SubConfigClass'; import {SubConfigClass} from 'typeconfig/src/decorators/class/SubConfigClass';
import { ConfigProperty } from 'typeconfig/src/decorators/property/ConfigPropoerty'; import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty';
export module ServerConfig { export module ServerConfig {
export enum DatabaseType { export enum DatabaseType {
@ -37,14 +37,14 @@ export module ServerConfig {
@SubConfigClass() @SubConfigClass()
export class MySQLConfig { export class MySQLConfig {
@ConfigProperty({envAlias: 'MYSQL_HOST'}) @ConfigProperty({envAlias: 'MYSQL_HOST'})
host: string = ''; host: string = 'localhost';
@ConfigProperty({envAlias: 'MYSQL_PORT'}) @ConfigProperty({envAlias: 'MYSQL_PORT', min: 0, max: 65535})
port: number = 3306; port: number = 3306;
@ConfigProperty({envAlias: 'MYSQL_DATABASE'}) @ConfigProperty({envAlias: 'MYSQL_DATABASE'})
database: string = ''; database: string = 'pigallery2';
@ConfigProperty({envAlias: 'MYSQL_USERNAME'}) @ConfigProperty({envAlias: 'MYSQL_USERNAME'})
username: string = ''; username: string = '';
@ConfigProperty({envAlias: 'MYSQL_PASSWORD'}) @ConfigProperty({envAlias: 'MYSQL_PASSWORD', type: 'password'})
password: string = ''; password: string = '';
} }
@ -93,13 +93,13 @@ export module ServerConfig {
@ConfigProperty({type: ReIndexingSensitivity}) @ConfigProperty({type: ReIndexingSensitivity})
reIndexingSensitivity: ReIndexingSensitivity = ReIndexingSensitivity.low; reIndexingSensitivity: ReIndexingSensitivity = ReIndexingSensitivity.low;
@ConfigProperty({ @ConfigProperty({
arrayType: String, arrayType: 'string',
description: 'If an entry starts with \'/\' it is treated as an absolute path.' + 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 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.' ' If it doesn\'t contain a \'/\', any folder with this name will be excluded.'
}) })
excludeFolderList: string[] = []; 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[] = []; excludeFileList: string[] = [];
} }
@ -291,9 +291,9 @@ export module ServerConfig {
@SubConfigClass() @SubConfigClass()
export class Config { export class Config {
@ConfigProperty({arrayType: String}) @ConfigProperty({arrayType: 'string'})
sessionSecret: string[] = []; sessionSecret: string[] = [];
@ConfigProperty({type: 'unsignedInt', envAlias: 'PORT'}) @ConfigProperty({type: 'unsignedInt', envAlias: 'PORT', min: 0, max: 65535})
port: number = 80; port: number = 80;
@ConfigProperty() @ConfigProperty()
host: string = '0.0.0.0'; host: string = '0.0.0.0';
@ -305,7 +305,7 @@ export module ServerConfig {
Database: DataBaseConfig = new DataBaseConfig(); Database: DataBaseConfig = new DataBaseConfig();
@ConfigProperty() @ConfigProperty()
Sharing: SharingConfig = new SharingConfig(); Sharing: SharingConfig = new SharingConfig();
@ConfigProperty({description: 'unit: ms'}) @ConfigProperty({type: 'unsignedInt', description: 'unit: ms'})
sessionTimeout: number = 1000 * 60 * 60 * 24 * 7; // in ms sessionTimeout: number = 1000 * 60 * 60 * 24 * 7; // in ms
@ConfigProperty() @ConfigProperty()
Indexing: IndexingConfig = new IndexingConfig(); Indexing: IndexingConfig = new IndexingConfig();

View File

@ -2,15 +2,14 @@
import 'reflect-metadata'; import 'reflect-metadata';
import {ClientConfig} from '../public/ClientConfig'; import {ClientConfig} from '../public/ClientConfig';
import {ServerConfig} from './PrivateConfig'; import {ServerConfig} from './PrivateConfig';
import {WebConfigClass} from 'typeconfig/src/decorators/class/WebConfigClass'; import {WebConfigClass} from 'typeconfig/web';
import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty'; import {ConfigProperty, ConfigState} from 'typeconfig/common';
import {ConfigDefaults} from 'typeconfig/src/decorators/property/ConfigDefaults';
@WebConfigClass() @WebConfigClass()
export class WebConfig { export class WebConfig {
@ConfigDefaults() @ConfigState()
Defaults: WebConfig; State: any;
@ConfigProperty() @ConfigProperty()
Server: ServerConfig.Config = new ServerConfig.Config(); Server: ServerConfig.Config = new ServerConfig.Config();

View File

@ -2,23 +2,23 @@
import 'reflect-metadata'; import 'reflect-metadata';
import {SortingMethods} from '../../entities/SortingMethods'; import {SortingMethods} from '../../entities/SortingMethods';
import {UserRoles} from '../../entities/UserDTO'; import {UserRoles} from '../../entities/UserDTO';
import { SubConfigClass } from 'typeconfig/src/decorators/class/SubConfigClass'; import {SubConfigClass} from 'typeconfig/src/decorators/class/SubConfigClass';
import { ConfigProperty } from 'typeconfig/src/decorators/property/ConfigPropoerty'; import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty';
export module ClientConfig { export module ClientConfig {
export enum MapProviders { export enum MapProviders {
OpenStreetMap = 0, Mapbox = 1, Custom = 2 OpenStreetMap = 1, Mapbox = 2, Custom = 3
} }
@SubConfigClass() @SubConfigClass()
export class AutoCompleteConfig { export class AutoCompleteConfig {
@ConfigProperty() @ConfigProperty()
enabled = true; enabled: boolean = true;
@ConfigProperty() @ConfigProperty({type: 'unsignedInt'})
maxItemsPerCategory = 5; maxItemsPerCategory: number = 5;
@ConfigProperty() @ConfigProperty({type: 'unsignedInt'})
cacheTimeout: number = 1000 * 60 * 60; cacheTimeout: number = 1000 * 60 * 60;
} }
@ -28,11 +28,11 @@ export module ClientConfig {
enabled: boolean = true; enabled: boolean = true;
@ConfigProperty() @ConfigProperty()
instantSearchEnabled: boolean = true; instantSearchEnabled: boolean = true;
@ConfigProperty() @ConfigProperty({type: 'unsignedInt'})
InstantSearchTimeout: number = 3000; InstantSearchTimeout: number = 3000;
@ConfigProperty() @ConfigProperty({type: 'unsignedInt'})
instantSearchCacheTimeout: number = 1000 * 60 * 60; instantSearchCacheTimeout: number = 1000 * 60 * 60;
@ConfigProperty() @ConfigProperty({type: 'unsignedInt'})
searchCacheTimeout: number = 1000 * 60 * 60; searchCacheTimeout: number = 1000 * 60 * 60;
@ConfigProperty() @ConfigProperty()
AutoComplete: AutoCompleteConfig = new AutoCompleteConfig(); AutoComplete: AutoCompleteConfig = new AutoCompleteConfig();
@ -66,7 +66,7 @@ export module ClientConfig {
enabled: boolean = true; enabled: boolean = true;
@ConfigProperty() @ConfigProperty()
useImageMarkers: boolean = true; useImageMarkers: boolean = true;
@ConfigProperty() @ConfigProperty({type: MapProviders})
mapProvider: MapProviders = MapProviders.OpenStreetMap; mapProvider: MapProviders = MapProviders.OpenStreetMap;
@ConfigProperty() @ConfigProperty()
mapboxAccessToken: string = ''; mapboxAccessToken: string = '';
@ -76,11 +76,11 @@ export module ClientConfig {
@SubConfigClass() @SubConfigClass()
export class ThumbnailConfig { export class ThumbnailConfig {
@ConfigProperty() @ConfigProperty({type: 'unsignedInt', max: 100})
iconSize: number = 45; iconSize: number = 45;
@ConfigProperty() @ConfigProperty({type: 'unsignedInt'})
personThumbnailSize: number = 200; personThumbnailSize: number = 200;
@ConfigProperty({arrayType: Number}) @ConfigProperty({arrayType: 'unsignedInt'})
thumbnailSizes: number[] = [240, 480]; thumbnailSizes: number[] = [240, 480];
@ConfigProperty({volatile: true}) @ConfigProperty({volatile: true})
concurrentThumbnailGenerations: number = 1; concurrentThumbnailGenerations: number = 1;
@ -184,7 +184,7 @@ export module ClientConfig {
authenticationRequired: boolean = true; authenticationRequired: boolean = true;
@ConfigProperty({type: UserRoles}) @ConfigProperty({type: UserRoles})
unAuthenticatedUserRole: UserRoles = UserRoles.Admin; unAuthenticatedUserRole: UserRoles = UserRoles.Admin;
@ConfigProperty({arrayType: String, volatile: true}) @ConfigProperty({arrayType: 'string', volatile: true})
languages: string[]; languages: string[];
@ConfigProperty() @ConfigProperty()
Media: MediaConfig = new MediaConfig(); 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 {JobButtonComponent} from './ui/settings/jobs/button/job-button.settings.component';
import {ErrorInterceptor} from './model/network/helper/error.interceptor'; import {ErrorInterceptor} from './model/network/helper/error.interceptor';
import {CSRFInterceptor} from './model/network/helper/csrf.interceptor'; import {CSRFInterceptor} from './model/network/helper/csrf.interceptor';
import {SettingsEntryComponent} from './ui/settings/_abstract/settings-entry/settings-entry.component';
@Injectable() @Injectable()
@ -187,6 +188,7 @@ export function translationsFactory(locale: string) {
DuplicateComponent, DuplicateComponent,
DuplicatesPhotoComponent, DuplicatesPhotoComponent,
// Settings // Settings
SettingsEntryComponent,
UserMangerSettingsComponent, UserMangerSettingsComponent,
DatabaseSettingsComponent, DatabaseSettingsComponent,
MapSettingsComponent, MapSettingsComponent,
@ -218,6 +220,7 @@ export function translationsFactory(locale: string) {
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true}, {provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
{provide: UrlSerializer, useClass: CustomUrlSerializer}, {provide: UrlSerializer, useClass: CustomUrlSerializer},
{provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig}, {provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig},
StringifySortingMethod,
NetworkService, NetworkService,
ShareService, ShareService,
UserService, UserService,

View File

@ -31,13 +31,15 @@
id="simplifiedMode" id="simplifiedMode"
class="switch" class="switch"
name="simplifiedMode" name="simplifiedMode"
[switch-off-color]="'warning'" switch-off-color="warning"
[switch-on-color]="'primary'" switch-on-color="primary"
[switch-inverse]="true" switch-inverse="true"
[switch-off-text]="text.Advanced" switch-off-text="Advanced"
[switch-on-text]="text.Simplified" switch-on-text="Simplified"
[switch-handle-width]="100" i18n-switch-off-text
[switch-label-width]="20" i18n-switch-on-text
switch-handle-width="100"
switch-label-width="20"
[(ngModel)]="simplifiedMode"> [(ngModel)]="simplifiedMode">
</bSwitch> </bSwitch>
</div> </div>

View File

@ -17,15 +17,11 @@ import {formatDate} from '@angular/common';
}) })
export class AdminComponent implements OnInit, AfterViewInit { export class AdminComponent implements OnInit, AfterViewInit {
simplifiedMode = true; simplifiedMode = true;
text = {
Advanced: 'Advanced',
Simplified: 'Simplified'
};
appVersion = Config.Client.appVersion; appVersion = Config.Client.appVersion;
versionExtra = ''; versionExtra = '';
upTime = Config.Client.upTime; upTime = Config.Client.upTime;
@ViewChildren('setting') settingsComponents: QueryList<ISettingsComponent>; @ViewChildren('setting') settingsComponents: QueryList<ISettingsComponent>;
@ViewChildren('setting', {read: ElementRef}) settingsComponents2: QueryList<ElementRef>; @ViewChildren('setting', {read: ElementRef}) settingsComponentsElemRef: QueryList<ElementRef>;
contents: ISettingsComponent[] = []; contents: ISettingsComponent[] = [];
constructor(private _authService: AuthenticationService, constructor(private _authService: AuthenticationService,
@ -33,8 +29,6 @@ export class AdminComponent implements OnInit, AfterViewInit {
public notificationService: NotificationService, public notificationService: NotificationService,
@Inject(LOCALE_ID) private locale: string, @Inject(LOCALE_ID) private locale: string,
public i18n: I18n) { public i18n: I18n) {
this.text.Advanced = i18n('Advanced');
this.text.Simplified = i18n('Simplified');
if (Config.Client.buildTime) { if (Config.Client.buildTime) {
this.versionExtra = i18n('Built at') + ': ' + formatDate(Config.Client.buildTime, 'medium', locale); this.versionExtra = i18n('Built at') + ': ' + formatDate(Config.Client.buildTime, 'medium', locale);
@ -50,13 +44,13 @@ export class AdminComponent implements OnInit, AfterViewInit {
} }
scrollTo(i: number) { scrollTo(i: number) {
PageHelper.ScrollY = this.settingsComponents2.toArray()[i].nativeElement.getBoundingClientRect().top + PageHelper.ScrollY = this.settingsComponentsElemRef.toArray()[i].nativeElement.getBoundingClientRect().top +
PageHelper.ScrollY; PageHelper.ScrollY;
} }
ngOnInit() { ngOnInit() {
if (!this._authService.isAuthenticated() if (!this._authService.isAuthenticated()
|| this._authService.user.value.role < UserRoles.Admin) { || this._authService.user.value.role < UserRoles.Admin) {
this._navigation.toLogin(); this._navigation.toLogin();
return; return;
} }

View File

@ -16,3 +16,13 @@
margin-bottom: -4px; 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 {Subscription} from 'rxjs';
import {ISettingsComponent} from './ISettingsComponent'; import {ISettingsComponent} from './ISettingsComponent';
import {WebConfig} from '../../../../../common/config/private/WebConfig'; 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>> export abstract class SettingsComponent<T extends { [key: string]: any }, S extends AbstractSettingsService<T> = AbstractSettingsService<T>>
implements OnInit, OnDestroy, OnChanges, ISettingsComponent { implements OnInit, OnDestroy, OnChanges, ISettingsComponent {
@ -20,7 +41,8 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
public simplifiedMode = true; public simplifiedMode = true;
@ViewChild('settingsForm', {static: true}) @ViewChild('settingsForm', {static: true})
form: HTMLFormElement; form: FormControl;
@Output() @Output()
hasAvailableSettings = true; hasAvailableSettings = true;
@ -28,14 +50,9 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
public inProgress = false; public inProgress = false;
public error: string = null; public error: string = null;
public changed = false; public changed = false;
public settings: T = <T>{}; public states: RecursiveState = <any>{};
public original: T = <T>{};
text = {
Enabled: 'Enabled',
Disabled: 'Disabled',
Low: 'Low',
High: 'High'
};
private _subscription: Subscription = null; private _subscription: Subscription = null;
private readonly _settingsSubscription: 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._settingsSubscription = this._settingsService.Settings.subscribe(this.onNewSettings);
this.onNewSettings(this._settingsService._settingsService.settings.value); 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) => { 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(); this.ngOnChanges();
}; };
@ -104,12 +131,32 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
return true; return true;
} }
public testSettingChanges() { onOptionChange = () => {
// TODO: fix after this issue is fixed: https://github.com/angular/angular/issues/24818
setTimeout(() => { 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); }, 0);
} };
ngOnInit() { ngOnInit() {
if (!this._authService.isAuthenticated() || 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 // TODO: fix after this issue is fixed: https://github.com/angular/angular/issues/24818
this._subscription = this.form.valueChanges.subscribe(() => { this._subscription = this.form.valueChanges.subscribe(() => {
this.testSettingChanges(); this.onOptionChange();
}); });
} }
ngOnChanges(): void { ngOnChanges(): void {
@ -146,11 +192,28 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
this.getSettings(); 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() { public async save() {
this.inProgress = true; this.inProgress = true;
this.error = ''; this.error = '';
try { try {
await this._settingsService.updateSettings(this.settings); await this._settingsService.updateSettings(this.stateToSettings());
await this.getSettings(); await this.getSettings();
this.notification.success(this.Name + ' ' + this.i18n('settings saved'), this.i18n('Success')); this.notification.success(this.Name + ' ' + this.i18n('settings saved'), this.i18n('Success'));
this.inProgress = false; this.inProgress = false;
@ -166,11 +229,11 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
return false; return false;
} }
private async getSettings() { private async getSettings() {
await this._settingsService.getSettings(); await this._settingsService.getSettings();
this.changed = false; 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 [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<div class="form-group row" [hidden]="simplifiedMode"> <app-settings-entry
<label class="col-md-2 control-label" for="applicationTitle" i18n>Page title</label> name="Page title"
<div class="col-md-10"> [ngModel]="states.applicationTitle"
<input type="text" class="form-control" placeholder="Pigallery 2" i18n-name
id="applicationTitle" required="true"
[(ngModel)]="settings.applicationTitle" [simplifiedMode]="simplifiedMode">
name="applicationTitle" required> </app-settings-entry>
</div>
</div>
<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"> <app-settings-entry
<label class="col-md-2 control-label" for="port" i18n>Port</label> name="Host"
<div class="col-md-10"> [ngModel]="states.host"
<input type="number" class="form-control" placeholder="80" description="Server will accept connections from this IPv6 or IPv4 address."
id="port" i18n-description i18n-name
min="0" placeholder="0.0.0.0"
step="1" required="true"
max="65535" [simplifiedMode]="simplifiedMode">
[(ngModel)]="settings.port" </app-settings-entry>
name="port" required>
<small class="form-text text-muted" i18n>Port number. Port 80 is usually what you need.</small>
</div>
</div>
<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"> <app-settings-entry
<label class="col-md-2 control-label" for="tempFolder" i18n>Temp folder</label> name="Port"
<div class="col-md-10"> description="Port number. Port 80 is usually what you need."
<input type="text" class="form-control" placeholder="path" i18n-description i18n-name
id="tempFolder" [ngModel]="states.port"
[(ngModel)]="settings.tempFolder" required="true"
name="tempFolder" required> [simplifiedMode]="simplifiedMode">
<small class="form-text text-muted" i18n>Thumbnails, coverted photos, videos will be stored here (write permission required)</small> </app-settings-entry>
</div>
</div>
<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"> <app-settings-entry
<label class="col-md-2 control-label" for="urlBase" i18n>Url Base</label> name="Images folder"
<div class="col-md-10"> description="Images are loaded from this folder (read permission required)"
<input type="url" class="form-control" placeholder="/myGallery" placeholder="path"
id="urlBase" i18n-description i18n-name
[(ngModel)]="settings.urlBase" required
(change)="onUrlBaseChanged()" [ngModel]="states.imagesFolder">
name="urlBase"> </app-settings-entry>
<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. <app-settings-entry
</small> name="Temp folder"
</div> description="Thumbnails, converted photos, videos will be stored here (write
</div> 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> <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. 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 { 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('http://', 'g'), '')
.replace(new RegExp('https://', 'g'), ''); .replace(new RegExp('https://', 'g'), '');
if (url.indexOf('/') !== -1) { if (url.indexOf('/') !== -1) {
@ -57,23 +57,24 @@ export class BasicSettingsComponent extends SettingsComponent<BasicConfigDTO> {
} }
checkUrlError() { checkUrlError() {
this.urlError = this.settings.urlBase !== this.calcBaseUrl(); this.urlError = this.states.urlBase.value !== this.calcBaseUrl();
} }
onUrlChanged() { onUrlChanged() {
console.log('called');
if (this.urlBaseChanged === false) { if (this.urlBaseChanged === false) {
this.settings.urlBase = this.calcBaseUrl(); this.states.urlBase.value = this.calcBaseUrl();
} else { } else {
this.checkUrlError(); this.checkUrlError();
} }
} }
onUrlBaseChanged() { onUrlBaseChanged = () => {
this.urlBaseChanged = true; this.urlBaseChanged = true;
this.checkUrlError(); this.checkUrlError();
} };
} }

View File

@ -6,71 +6,68 @@
<div class="card-body"> <div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div> <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"> <app-settings-entry
<label class="col-md-2 control-label" for="dbFolder" i18n>Database folder</label> name="Database folder"
<div class="col-md-10"> description="All file-based data will be stored here (sqlite database, user database in case of memory db, job history data)"
<input type="text" class="form-control" placeholder="db" [ngModel]="states.dbFolder"
[(ngModel)]="settings.dbFolder" id="dbFolder" name="dbFolder" required> i18n-name i18n-description
<small class="form-text text-muted" i18n> required="true">
All file-based data will be stored here (sqlite database, user database in case of memory db, job history </app-settings-entry>
data)
</small>
</div>
</div>
<ng-container *ngIf="settings.type == DatabaseType.mysql">
<div class="form-group row"> <ng-container *ngIf="states.type.value == DatabaseType.mysql">
<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" <app-settings-entry
[(ngModel)]="settings.mysql.host" id="mysql_host" name="mysql_host" required> name="Host"
</div> [ngModel]="states.mysql.host"
</div> i18n-name
<div class="form-group row"> required="true">
<label class="col-md-2 control-label" for="mysql_port" i18n>Port</label> </app-settings-entry>
<div class="col-md-10">
<input type="number" class="form-control" placeholder="3306" min="0" max="65535" <app-settings-entry
[(ngModel)]="settings.mysql.port" id="mysql_port" name="mysql_port" required> name="Port"
</div> [ngModel]="states.mysql.port"
</div> 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> </ng-container>

View File

@ -1,62 +1,49 @@
<form #settingsForm="ngForm"> <form #settingsForm="ngForm">
<div class="card mb-4" <div class="card mb-4"
[ngClass]="settings.enabled && !_settingsService.isSupported()?'panel-warning':''"> [ngClass]="states.enabled.value && !_settingsService.isSupported()?'panel-warning':''">
<h5 class="card-header"> <h5 class="card-header">
{{Name}} {{Name}}
<div class="switch-wrapper"> <div class="switch-wrapper"
[class.changed-settings]="states.enabled.value != states.enabled.default">
<bSwitch <bSwitch
class="switch" class="switch"
name="enabled" name="enabled"
[switch-on-color]="'success'" switch-on-color="success"
[switch-inverse]="true" switch-inverse="true"
[switch-off-text]="text.Disabled" switch-off-text="Disabled"
[switch-on-text]="text.Enabled" switch-on-text="Enabled"
[switch-disabled]="inProgress || (!settings.enabled && !_settingsService.isSupported())" i18n-switch-off-text
[switch-handle-width]="100" i18n-switch-on-text
[switch-label-width]="20" [switch-disabled]="inProgress || (!states.enabled.value && !_settingsService.isSupported())"
[(ngModel)]="settings.enabled"> switch-handle-width="100"
switch-label-width="20"
[(ngModel)]="states.enabled.value">
</bSwitch> </bSwitch>
</div> </div>
</h5> </h5>
<div class="card-body"> <div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div> <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="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>
<div class="form-group row"> <app-settings-entry
<label class="col-md-2 control-label" for="writeAccessMinRole" i18n>Face starring right</label> name="Override keywords"
<div class="col-md-10"> [ngModel]="states.keywordsToPersons"
<select class="form-control" [(ngModel)]="settings.writeAccessMinRole" name="writeAccessMinRole" id="writeAccessMinRole" required> description="If a photo has the same face (person) name and keyword, the app removes the duplicate, keeping the face only."
<option *ngFor="let repository of userRoles" [value]="repository.key">{{repository.value}} i18n-name i18n-description>
</option> </app-settings-entry>
</select>
<small class="form-text text-muted" i18n>Required minimum right to start (favourite) a face.</small> <app-settings-entry
</div> name="Face starring right"
</div> [ngModel]="states.writeAccessMinRole"
description="Required minimum right to start (favourite) a face."
i18n-name i18n-description>
</app-settings-entry>
</ng-container> </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. Faces are not supported with these settings.
</div> </div>
<button class="btn btn-success float-right" <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> <div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<ng-container *ngIf="!simplifiedMode"> <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"> <app-settings-entry
<label class="col-md-2 control-label" for="reIndexingSensitivity" i18n>Folder reindexing sensitivity</label> name="Index cache timeout [ms]"
<div class="col-md-10"> description="If there was no indexing in this time, it reindexes. (skipped if indexes are in DB and sensitivity is low)"
<select id="reIndexingSensitivity" class="form-control" [(ngModel)]="settings.reIndexingSensitivity" i18n-description i18n-name
name="reIndexingSensitivity" required> [ngModel]="states.cachedFolderTimeout"
<option *ngFor="let type of types" [ngValue]="type.key">{{type.value}} required="true">
</option> </app-settings-entry>
</select>
<small <app-settings-entry
class="form-text text-muted" name="Sub folder preview size"
i18n>Set the reindexing sensitivity. High value check the folders for change more often. description="Reads this many photos from sub folders."
</small> i18n-description i18n-name
</div> [ngModel]="states.folderPreviewSize"
</div> 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" <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])]; 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() { ngOnDestroy() {
super.ngOnDestroy(); super.ngOnDestroy();

View File

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

View File

@ -100,7 +100,7 @@ export class JobsSettingsComponent extends SettingsComponent<ServerConfig.JobCon
} }
remove(schedule: JobScheduleDTO) { 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) { 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; return curr && curr.trigger.type === JobTriggerType.after && prev && prev.name === curr.trigger.afterScheduleName;
} }
public sortedSchedules() { public sortedSchedules(): JobScheduleDTO[] {
return this.settings.scheduled.slice().sort((a, b) => { return this.states.scheduled.value.slice().sort((a: JobScheduleDTO, b: JobScheduleDTO) => {
return this.getNextRunningDate(a, this.settings.scheduled) - this.getNextRunningDate(b, this.settings.scheduled); return this.getNextRunningDate(a, this.states.scheduled.value) - this.getNextRunningDate(b, this.states.scheduled.value);
}); });
} }
addNewJob() { addNewJob() {
const jobName = this.newSchedule.jobName; 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.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(); this.jobModal.hide();
} }

View File

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

View File

@ -33,14 +33,14 @@ export class MapSettingsComponent extends SettingsComponent<ClientConfig.MapConf
addNewLayer() { addNewLayer() {
this.settings.customLayers.push({ this.states.customLayers.value.push({
name: 'Layer-' + this.settings.customLayers.length, name: 'Layer-' + this.states.customLayers.value.length,
url: '' url: ''
}); });
} }
removeLayer(layer: ClientConfig.MapLayers) { 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 <bSwitch
class="switch" class="switch"
name="enabled" name="enabled"
[switch-on-color]="'success'" switch-on-color="success"
[switch-inverse]="true" switch-inverse="true"
[switch-off-text]="text.Disabled" switch-off-text="Disabled"
[switch-on-text]="text.Enabled" switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
[switch-disabled]="inProgress" [switch-disabled]="inProgress"
[switch-handle-width]="100" switch-handle-width="100"
[switch-label-width]="20" switch-label-width="20"
[(ngModel)]="settings.enabled"> [(ngModel)]="states.enabled.value">
</bSwitch> </bSwitch>
</div> </div>
</h5> </h5>

View File

@ -8,161 +8,89 @@
<p class="title" i18n>Threads:</p> <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> <app-settings-entry
<div class="col-md-10"> name="Threading"
<select id="thumbnailThreads" class="form-control" [(ngModel)]="settings.Server.thumbnailThreads" description="Runs directory scanning and thumbnail generation (only for Jimp) in a different thread."
name="Server[thumbnailThreads]" required> i18n-description i18n-name
<option [ngValue]="0">auto</option> [ngModel]="states.Server.enabled"
<option *ngFor="let i of threads" [ngValue]="i">{{i}}</option> required="true">
</select> </app-settings-entry>
<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="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/> <hr/>
<p class="title" i18n>Misc:</p> <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"> <app-settings-entry
<label class="col-md-2 control-label" for="enableOnScrollRendering" i18n>Lazy image rendering</label> name="Scroll based thumbnail generation"
<div class="col-md-10"> description="Those thumbnails get higher priority that are visible on the screen."
<bSwitch i18n-description i18n-name
id="enableOnScrollRendering" [ngModel]="states.Client.enableOnScrollThumbnailPrioritising"
class="switch" required="true">
name="enableOnScrollRendering" </app-settings-entry>
[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="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"> <app-settings-entry
<label class="col-md-2 control-label" for="enableCache" i18n>Cache</label> name="Cache"
<div class="col-md-10"> description="Caches directory contents and search results for better performance."
<bSwitch i18n-description i18n-name
id="enableCache" [ngModel]="states.Client.enableCache"
class="switch" required="true">
name="enableCache" </app-settings-entry>
[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="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/> <hr/>
<p class="title" i18n>Navigation bar:</p> <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> <app-settings-entry
<div class="col-md-10"> name="Show item count"
<bSwitch description="Show the number of items (photos) in the folder."
id="showItemCount" i18n-description i18n-name
class="switch" [ngModel]="states.Client.NavBar.showItemCount"
name="showItemCount" [simplifiedMode]="simplifiedMode"
[switch-on-color]="'primary'" required="true">
[switch-inverse]="true" </app-settings-entry>
[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>
<div class="form-group row" [hidden]="simplifiedMode">
<label class="col-md-2 control-label" for="defaultPhotoSortingMethod" i18n>Default photo sorting method</label> <app-settings-entry
<div class="col-md-10"> name="Default photo sorting method"
<select id="defaultPhotoSortingMethod" class="form-control" [(ngModel)]="settings.Client.defaultPhotoSortingMethod" i18n-name
name="defaultPhotoSortingMethod" required> [ngModel]="states.Client.defaultPhotoSortingMethod"
<option *ngFor="let type of types" [ngValue]="type.key">{{type.key | stringifySorting}} [optionMap]="sortingMap"
</option> [simplifiedMode]="simplifiedMode"
</select> required="true">
</div> </app-settings-entry>
</div>
<button class="btn btn-success float-right" <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 {I18n} from '@ngx-translate/i18n-polyfill';
import {Utils} from '../../../../../common/Utils'; import {Utils} from '../../../../../common/Utils';
import {SortingMethods} from '../../../../../common/entities/SortingMethods'; import {SortingMethods} from '../../../../../common/entities/SortingMethods';
import {StringifySortingMethod} from '../../../pipes/StringifySortingMethod';
@Component({ @Component({
selector: 'app-settings-other', selector: 'app-settings-other',
@ -20,17 +21,24 @@ export class OtherSettingsComponent extends SettingsComponent<OtherConfigDTO> im
types: { key: number; value: string }[] = []; 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, constructor(_authService: AuthenticationService,
_navigation: NavigationService, _navigation: NavigationService,
_settingsService: OtherSettingsService, _settingsService: OtherSettingsService,
notification: NotificationService, notification: NotificationService,
i18n: I18n) { i18n: I18n,
private formatter: StringifySortingMethod) {
super(i18n('Other'), _authService, _navigation, _settingsService, notification, i18n, s => ({ super(i18n('Other'), _authService, _navigation, _settingsService, notification, i18n, s => ({
Server: s.Server.Threading, Server: s.Server.Threading,
Client: s.Client.Other 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.types = Utils.enumToArray(SortingMethods);
this.hasAvailableSettings = !this.simplifiedMode; this.hasAvailableSettings = !this.simplifiedMode;
} }

View File

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

View File

@ -6,7 +6,6 @@ import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service'; import {NotificationService} from '../../../model/notification.service';
import {I18n} from '@ngx-translate/i18n-polyfill'; import {I18n} from '@ngx-translate/i18n-polyfill';
import {ScheduledJobsService} from '../scheduled-jobs.service'; import {ScheduledJobsService} from '../scheduled-jobs.service';
import {Utils} from '../../../../../common/Utils';
import {DefaultsJobs, JobDTO} from '../../../../../common/entities/job/JobDTO'; import {DefaultsJobs, JobDTO} from '../../../../../common/entities/job/JobDTO';
import {JobProgressStates} from '../../../../../common/entities/job/JobProgressDTO'; import {JobProgressStates} from '../../../../../common/entities/job/JobProgressDTO';
import {ServerConfig} from '../../../../../common/config/private/PrivateConfig'; import {ServerConfig} from '../../../../../common/config/private/PrivateConfig';
@ -25,19 +24,11 @@ export class PhotoSettingsComponent extends SettingsComponent<{
server: ServerConfig.PhotoConfig, server: ServerConfig.PhotoConfig,
client: ClientConfig.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; PhotoProcessingLib = ServerConfig.PhotoProcessingLib;
JobProgressStates = JobProgressStates; 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']]; readonly jobName = DefaultsJobs[DefaultsJobs['Photo Converting']];
constructor(_authService: AuthenticationService, constructor(_authService: AuthenticationService,
@ -52,15 +43,24 @@ export class PhotoSettingsComponent extends SettingsComponent<{
server: s.Server.Media.Photo server: s.Server.Media.Photo
})); }));
const currentRes = _settingsService.Settings.value.Server.Media.Photo.Converting.resolution; const currentRes = _settingsService.Settings.value.Server.Media.Photo.Converting.resolution;
if (this.resolutions.indexOf(currentRes) === -1) { if (this.resolutionTypes.indexOf(currentRes) === -1) {
this.resolutions.push(currentRes); this.resolutionTypes.push(currentRes);
} }
this.resolutions = this.resolutionTypes.map(e => ({key: e, value: e + 'px'}));
} }
get Progress() { get Progress() {
return this.jobsService.progress.value[JobDTO.getHashName(DefaultsJobs[DefaultsJobs['Photo Converting']])]; 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"> <form #settingsForm="ngForm">
<div class="card mb-4" <div class="card mb-4"
[ngClass]="settings.enabled && !_settingsService.isSupported()?'panel-warning':''"> [ngClass]="states.enabled.value && !_settingsService.isSupported()?'panel-warning':''">
<h5 class="card-header"> <h5 class="card-header">
{{Name}} {{Name}}
<div class="switch-wrapper"> <div class="switch-wrapper">
<bSwitch <bSwitch
class="switch" class="switch"
name="enabled" name="enabled"
[switch-on-color]="'success'" switch-on-color="success"
[switch-inverse]="true" switch-inverse="true"
[switch-off-text]="text.Disabled" switch-off-text="Disabled"
[switch-on-text]="text.Enabled" switch-on-text="Enabled"
[switch-disabled]="inProgress || (!settings.enabled && !_settingsService.isSupported())" i18n-switch-off-text
[switch-handle-width]="100" i18n-switch-on-text
[switch-label-width]="20" [switch-disabled]="inProgress || !_settingsService.isSupported()"
[(ngModel)]="settings.enabled"> switch-handle-width="100"
switch-label-width="20"
[(ngModel)]="states.enabled.value">
</bSwitch> </bSwitch>
</div> </div>
</h5> </h5>
<div class="card-body"> <div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div> <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> <div class="alert alert-secondary" role="alert" i18n>
This feature enables you to generate 'random photo' urls. This feature enables you to generate 'random photo' urls.
@ -30,7 +32,7 @@
</div> </div>
</ng-container> </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 Random Photo is not supported with these settings
</div> </div>
<button class="btn btn-success float-right" <button class="btn btn-success float-right"

View File

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

View File

@ -1,50 +1,41 @@
<form #settingsForm="ngForm"> <form #settingsForm="ngForm">
<div class="card mb-4" <div class="card mb-4"
[ngClass]="settings.enabled && !_settingsService.isSupported()?'panel-warning':''"> [ngClass]="states.enabled.value && !_settingsService.isSupported()?'panel-warning':''">
<h5 class="card-header"> <h5 class="card-header">
{{Name}} {{Name}}
<div class="switch-wrapper"> <div class="switch-wrapper">
<bSwitch <bSwitch
class="switch" class="switch"
name="enabled" name="enabled"
[switch-on-color]="'success'" switch-on-color="success"
[switch-inverse]="true" switch-inverse="true"
[switch-off-text]="text.Disabled" switch-off-text="Disabled"
[switch-on-text]="text.Enabled" switch-on-text="Enabled"
[switch-disabled]="inProgress || (!settings.enabled && !_settingsService.isSupported())" i18n-switch-off-text
[switch-handle-width]="100" i18n-switch-on-text
[switch-label-width]="20" [switch-disabled]="inProgress || !_settingsService.isSupported()"
[(ngModel)]="settings.enabled"> switch-handle-width="100"
switch-label-width="20"
[(ngModel)]="states.enabled.value">
</bSwitch> </bSwitch>
</div> </div>
</h5> </h5>
<div class="card-body"> <div class="card-body">
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div> <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="form-group row">
<label class="col-md-2 control-label" for="passwordProtected" i18n>Password protected</label> <app-settings-entry
<div class="col-md-10"> name="Password protected"
<bSwitch description="Enables password protected sharing links."
id="passwordProtected" i18n-description i18n-name
class="switch" [ngModel]="states.passwordProtected"
name="passwordProtected" required="true">
[switch-on-color]="'primary'" </app-settings-entry>
[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>
</div>
</div>
</ng-container> </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 Sharing is not supported with these settings
</div> </div>
<button class="btn btn-success float-right" <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 [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
<div class="form-group row" [hidden]="simplifiedMode"> <app-settings-entry
<label class="col-md-2 control-label" for="quality" i18n>Thumbnail Quality</label> name="Thumbnail Quality"
<div class="col-md-10"> description="High quality may be slow. Especially with Jimp."
<bSwitch i18n-description i18n-name
id="quality" [ngModel]="states.server.qualityPriority"
class="switch" [simplifiedMode]="simplifiedMode"
name="enabled" required="true">
[switch-on-color]="'primary'" </app-settings-entry>
[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>
</div>
</div>
<div class="form-group row" [hidden]="simplifiedMode"> <app-settings-entry
<label class="col-md-2 control-label" for="icon" i18n>Icon size</label> name="Icon size"
<div class="col-md-10"> description="Icon size (used on maps)."
<input type="number" class="form-control" placeholder="30" i18n-description i18n-name
id="icon" [ngModel]="states.client.iconSize"
[(ngModel)]="settings.client.iconSize" [simplifiedMode]="simplifiedMode"
min="1" required="true">
max="100" </app-settings-entry>
step="1"
name="icon" required>
<small class="form-text text-muted" i18n>Icon size (used on maps)</small> <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" <button class="btn btn-success float-right"
[disabled]="!settingsForm.form.valid || !changed || inProgress" [disabled]="!settingsForm.form.valid || !changed || inProgress"

View File

@ -37,20 +37,9 @@ export class ThumbnailSettingsComponent
} }
get Config(): any { 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() { get Progress() {
return this.jobsService.progress.value[JobDTO.getHashName(this.jobName, this.Config)]; return this.jobsService.progress.value[JobDTO.getHashName(this.jobName, this.Config)];

View File

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

View File

@ -7,14 +7,16 @@
<bSwitch <bSwitch
class="switch" class="switch"
name="enabled" name="enabled"
[switch-on-color]="'success'" switch-on-color="success"
[switch-inverse]="true" switch-inverse="true"
[switch-off-text]="text.Disabled" switch-off-text="Disabled"
[switch-on-text]="text.Enabled" switch-on-text="Enabled"
i18n-switch-off-text
i18n-switch-on-text
[switch-disabled]="inProgress" [switch-disabled]="inProgress"
[switch-handle-width]="100" switch-handle-width="100"
[switch-label-width]="20" switch-label-width="20"
[(ngModel)]="settings.client.enabled"> [(ngModel)]="states.client.enabled.value">
</bSwitch> </bSwitch>
</div> </div>
</h5> </h5>
@ -42,58 +44,50 @@
</ng-container>&nbsp; </ng-container>&nbsp;
</div> </div>
<div class="form-group row" [hidden]="simplifiedMode"> <app-settings-entry
<label class="col-md-2 control-label" for="format" i18n>Format</label> name="Format"
<div class="col-md-10"> i18n-name
<select id="format" class="form-control" [(ngModel)]="settings.server.transcoding.format" [ngModel]="states.server.transcoding.format"
(ngModelChange)="formatChanged($event)" [simplifiedMode]="simplifiedMode"
name="format" required> (ngModelChange)="formatChanged($event)"
<option *ngFor="let format of formats" [ngValue]="format">{{format}} [options]="formats"
</option> required="true">
</select> </app-settings-entry>
</div>
</div>
<div class="form-group row" [hidden]="simplifiedMode"> <app-settings-entry
<label class="col-md-2 control-label" for="codec" i18n>Codec</label> name="Codec"
<div class="col-md-10"> i18n-name
<select id="codec" class="form-control" [(ngModel)]="settings.server.transcoding.codec" [ngModel]="states.server.transcoding.codec"
name="codec" required> [simplifiedMode]="simplifiedMode"
<option *ngFor="let codec of codecs[settings.server.transcoding.format]" [ngValue]="codec">{{codec}} [options]="codecs[states.server.transcoding.format.value]"
</option> required="true">
</select> </app-settings-entry>
</div>
</div>
<div class="form-group row"> <app-settings-entry
<label class="col-md-2 control-label" for="resolution" i18n>Resolution</label> name="Resolution"
<div class="col-md-10"> description="The height of the output video will be scaled down to this, while keeping the aspect ratio."
<select id="resolution" class="form-control" [(ngModel)]="settings.server.transcoding.resolution" i18n-name i18n-description
(ngModelChange)="updateBitRate()" [ngModel]="states.server.transcoding.resolution"
name="resolution" required> (ngModelChange)="updateBitRate()"
<option *ngFor="let resolution of resolutions" [ngValue]="resolution">{{resolution}}p [options]="resolutions"
</option> required="true">
</select> </app-settings-entry>
<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>
<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> <label class="col-md-2 control-label" for="bitRate" i18n>Bit rate</label>
<div class="col-md-10"> <div class="col-md-10">
<div class="input-group"> <div class="input-group">
@ -115,9 +109,6 @@
</div> </div>
<button class="btn btn-success float-right" <button class="btn btn-success float-right"
[disabled]="!settingsForm.form.valid || !changed || inProgress" [disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save (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 }> { export class VideoSettingsComponent extends SettingsComponent<{ server: ServerConfig.VideoConfig, client: ClientConfig.VideoConfig }> {
resolutions: ServerConfig.resolutionType[] = [360, 480, 720, 1080, 1440, 2160, 4320]; readonly resolutionTypes: 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']; resolutions: { key: number, value: string }[] = [];
fps = [24, 25, 30, 48, 50, 60]; 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; JobProgressStates = JobProgressStates;
readonly jobName = DefaultsJobs[DefaultsJobs['Video Converting']]; 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; const currentRes = _settingsService.Settings.value.Server.Media.Video.transcoding.resolution;
if (this.resolutions.indexOf(currentRes) === -1) { if (this.resolutionTypes.indexOf(currentRes) === -1) {
this.resolutions.push(currentRes); 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 { get bitRate(): number {
return this.settings.server.transcoding.bitRate / 1024 / 1024; return this.states.server.transcoding.bitRate.value / 1024 / 1024;
} }
set bitRate(value: number) { 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) { getRecommendedBitRate(resolution: number, fps: number) {
@ -83,12 +90,12 @@ export class VideoSettingsComponent extends SettingsComponent<{ server: ServerCo
} }
updateBitRate() { updateBitRate() {
this.settings.server.transcoding.bitRate = this.getRecommendedBitRate(this.settings.server.transcoding.resolution, this.states.server.transcoding.bitRate.value = this.getRecommendedBitRate(this.states.server.transcoding.resolution.value,
this.settings.server.transcoding.fps); this.states.server.transcoding.fps.value);
} }
formatChanged(format: ServerConfig.formatType) { 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; margin-left: -4px;
} }
.changed-settings .bootstrap-switch{
border-color: #007bff;
border-width: 1px;
}
#toast-container > div { #toast-container > div {
opacity:1; 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.res.should.have.status(200);
result.body.should.be.a('object'); result.body.should.be.a('object');
should.equal(result.body.error, null); 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}))));
}); });
}); });