mirror of
https://github.com/bpatrik/pigallery2.git
synced 2024-12-12 11:15:22 +02:00
Merge pull request #135 from bpatrik/develop
implementing advanced settings
This commit is contained in:
commit
399dbfb53c
8
package-lock.json
generated
8
package-lock.json
generated
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pigallery2",
|
||||
"version": "1.8.0",
|
||||
"version": "1.8.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
@ -18721,9 +18721,9 @@
|
||||
}
|
||||
},
|
||||
"typeconfig": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/typeconfig/-/typeconfig-2.0.0.tgz",
|
||||
"integrity": "sha512-IwZH+P8J4qhyrOfKzJZJx6raEkaBjjZIiE+rzrWgdOhqVhO5Cv+pkOQdNo+z/Wq5wER5YWeDNX7Wdbqv0jt6IA==",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typeconfig/-/typeconfig-2.0.6.tgz",
|
||||
"integrity": "sha512-OnXPXSDaK1mzH6dJ1HB9Q70ruJYngEhemwL9Y8+nG5E40Je4MMODuEY+tfjMtIiFIN772DU5Q+NebulLZNDjpw==",
|
||||
"requires": {
|
||||
"optimist": "0.6.1"
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pigallery2",
|
||||
"version": "1.8.0",
|
||||
"version": "1.8.1",
|
||||
"description": "This is a photo gallery optimised for running low resource servers (especially on raspberry pi)",
|
||||
"author": "Patrik J. Braun",
|
||||
"homepage": "https://github.com/bpatrik/PiGallery2",
|
||||
@ -16,6 +16,7 @@
|
||||
"coverage": "nyc report --reporter=text-lcov | coveralls",
|
||||
"start": "node ./src/backend/index",
|
||||
"run-dev": "ng build --aot --watch --output-path=./dist --i18n-locale en --i18n-file src/frontend/translate/messages.en.xlf --i18n-missing-translation warning",
|
||||
"run-dev-hu": "ng build --aot --watch --output-path=./dist --i18n-locale hu --i18n-file src/frontend/translate/messages.hu.xlf --i18n-missing-translation warning",
|
||||
"build-stats": "ng build --aot --prod --stats-json --output-path=./dist --i18n-locale en --i18n-file src/frontend/translate/messages.en.xlf --i18n-missing-translation warning",
|
||||
"merge-new-translation": "gulp merge-new-translation",
|
||||
"add-translation": "gulp add-translation",
|
||||
@ -47,7 +48,7 @@
|
||||
"sqlite3": "4.1.1",
|
||||
"ts-exif-parser": "0.1.4",
|
||||
"ts-node-iptc": "1.0.11",
|
||||
"typeconfig": "2.0.0",
|
||||
"typeconfig": "2.0.6",
|
||||
"typeorm": "0.2.21",
|
||||
"winston": "2.4.4"
|
||||
},
|
||||
|
@ -63,7 +63,10 @@ export class RenderingMWs {
|
||||
public static async renderConfig(req: Request, res: Response, next: NextFunction) {
|
||||
const originalConf = await Config.original();
|
||||
originalConf.Server.sessionSecret = null;
|
||||
const message = new Message<PrivateConfigClass>(null, <any>originalConf.toJSON({attachDefaults: true}));
|
||||
const message = new Message<PrivateConfigClass>(null, <any>originalConf.toJSON({
|
||||
attachState: true,
|
||||
attachVolatile: true
|
||||
}));
|
||||
res.json(message);
|
||||
}
|
||||
|
||||
|
@ -82,7 +82,7 @@ export class PublicRouter {
|
||||
res.tpl.user.csrfToken = req.csrfToken();
|
||||
}
|
||||
}
|
||||
res.tpl.clientConfig = {Client: Config.Client};
|
||||
res.tpl.clientConfig = {Client: Config.Client.toJSON({attachVolatile: true})};
|
||||
|
||||
return next();
|
||||
});
|
||||
|
@ -61,6 +61,10 @@ export class Utils {
|
||||
if (!object) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(object) && object.length !== filter.length) {
|
||||
return false;
|
||||
}
|
||||
const keys = Object.keys(filter);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
@ -158,10 +162,7 @@ export class Utils {
|
||||
});
|
||||
}
|
||||
|
||||
public static enumToArray(EnumType: any): Array<{
|
||||
key: number;
|
||||
value: string;
|
||||
}> {
|
||||
public static enumToArray(EnumType: any): { key: number; value: string }[] {
|
||||
const arr: Array<{ key: number; value: string; }> = [];
|
||||
for (const enumMember in EnumType) {
|
||||
if (!EnumType.hasOwnProperty(enumMember)) {
|
||||
|
@ -2,10 +2,8 @@ import {IPrivateConfig, ServerConfig} from './PrivateConfig';
|
||||
import {ClientConfig} from '../public/ClientConfig';
|
||||
import * as crypto from 'crypto';
|
||||
import * as path from 'path';
|
||||
import {ConfigClass} from 'typeconfig/src/decorators/class/ConfigClass';
|
||||
import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty';
|
||||
import {IConfigClass} from 'typeconfig/src/decorators/class/IConfigClass';
|
||||
import {ConfigClassBuilder} from 'typeconfig/src/decorators/builders/ConfigClassBuilder';
|
||||
import {ConfigClass, ConfigClassBuilder} from 'typeconfig/node';
|
||||
import {ConfigProperty, IConfigClass} from 'typeconfig/common';
|
||||
|
||||
|
||||
@ConfigClass({
|
||||
@ -16,7 +14,7 @@ import {ConfigClassBuilder} from 'typeconfig/src/decorators/builders/ConfigClass
|
||||
cli: {
|
||||
enable: {
|
||||
configPath: true,
|
||||
attachDefaults: true,
|
||||
attachState: true,
|
||||
attachDescription: true,
|
||||
rewriteCLIConfig: true,
|
||||
rewriteENVConfig: true,
|
||||
@ -33,7 +31,7 @@ export class PrivateConfigClass implements IPrivateConfig {
|
||||
@ConfigProperty()
|
||||
Server: ServerConfig.Config = new ServerConfig.Config();
|
||||
@ConfigProperty()
|
||||
Client: ClientConfig.Config = new ClientConfig.Config();
|
||||
Client: IConfigClass & ClientConfig.Config = <IConfigClass & ClientConfig.Config>(new ClientConfig.Config());
|
||||
|
||||
constructor() {
|
||||
if (!this.Server.sessionSecret || this.Server.sessionSecret.length === 0) {
|
||||
|
@ -37,14 +37,14 @@ export module ServerConfig {
|
||||
@SubConfigClass()
|
||||
export class MySQLConfig {
|
||||
@ConfigProperty({envAlias: 'MYSQL_HOST'})
|
||||
host: string = '';
|
||||
@ConfigProperty({envAlias: 'MYSQL_PORT'})
|
||||
host: string = 'localhost';
|
||||
@ConfigProperty({envAlias: 'MYSQL_PORT', min: 0, max: 65535})
|
||||
port: number = 3306;
|
||||
@ConfigProperty({envAlias: 'MYSQL_DATABASE'})
|
||||
database: string = '';
|
||||
database: string = 'pigallery2';
|
||||
@ConfigProperty({envAlias: 'MYSQL_USERNAME'})
|
||||
username: string = '';
|
||||
@ConfigProperty({envAlias: 'MYSQL_PASSWORD'})
|
||||
@ConfigProperty({envAlias: 'MYSQL_PASSWORD', type: 'password'})
|
||||
password: string = '';
|
||||
}
|
||||
|
||||
@ -93,13 +93,13 @@ export module ServerConfig {
|
||||
@ConfigProperty({type: ReIndexingSensitivity})
|
||||
reIndexingSensitivity: ReIndexingSensitivity = ReIndexingSensitivity.low;
|
||||
@ConfigProperty({
|
||||
arrayType: String,
|
||||
arrayType: 'string',
|
||||
description: 'If an entry starts with \'/\' it is treated as an absolute path.' +
|
||||
' If it doesn\'t start with \'/\' but contains a \'/\', the path is relative to the image directory.' +
|
||||
' If it doesn\'t contain a \'/\', any folder with this name will be excluded.'
|
||||
})
|
||||
excludeFolderList: string[] = [];
|
||||
@ConfigProperty({arrayType: String, description: 'Any folder that contains a file with this name will be excluded from indexing.'})
|
||||
@ConfigProperty({arrayType: 'string', description: 'Any folder that contains a file with this name will be excluded from indexing.'})
|
||||
excludeFileList: string[] = [];
|
||||
}
|
||||
|
||||
@ -291,9 +291,9 @@ export module ServerConfig {
|
||||
|
||||
@SubConfigClass()
|
||||
export class Config {
|
||||
@ConfigProperty({arrayType: String})
|
||||
@ConfigProperty({arrayType: 'string'})
|
||||
sessionSecret: string[] = [];
|
||||
@ConfigProperty({type: 'unsignedInt', envAlias: 'PORT'})
|
||||
@ConfigProperty({type: 'unsignedInt', envAlias: 'PORT', min: 0, max: 65535})
|
||||
port: number = 80;
|
||||
@ConfigProperty()
|
||||
host: string = '0.0.0.0';
|
||||
@ -305,7 +305,7 @@ export module ServerConfig {
|
||||
Database: DataBaseConfig = new DataBaseConfig();
|
||||
@ConfigProperty()
|
||||
Sharing: SharingConfig = new SharingConfig();
|
||||
@ConfigProperty({description: 'unit: ms'})
|
||||
@ConfigProperty({type: 'unsignedInt', description: 'unit: ms'})
|
||||
sessionTimeout: number = 1000 * 60 * 60 * 24 * 7; // in ms
|
||||
@ConfigProperty()
|
||||
Indexing: IndexingConfig = new IndexingConfig();
|
||||
|
@ -2,15 +2,14 @@
|
||||
import 'reflect-metadata';
|
||||
import {ClientConfig} from '../public/ClientConfig';
|
||||
import {ServerConfig} from './PrivateConfig';
|
||||
import {WebConfigClass} from 'typeconfig/src/decorators/class/WebConfigClass';
|
||||
import {ConfigProperty} from 'typeconfig/src/decorators/property/ConfigPropoerty';
|
||||
import {ConfigDefaults} from 'typeconfig/src/decorators/property/ConfigDefaults';
|
||||
import {WebConfigClass} from 'typeconfig/web';
|
||||
import {ConfigProperty, ConfigState} from 'typeconfig/common';
|
||||
|
||||
|
||||
@WebConfigClass()
|
||||
export class WebConfig {
|
||||
@ConfigDefaults()
|
||||
Defaults: WebConfig;
|
||||
@ConfigState()
|
||||
State: any;
|
||||
|
||||
@ConfigProperty()
|
||||
Server: ServerConfig.Config = new ServerConfig.Config();
|
||||
|
@ -9,16 +9,16 @@ import { ConfigProperty } from 'typeconfig/src/decorators/property/ConfigPropoer
|
||||
export module ClientConfig {
|
||||
|
||||
export enum MapProviders {
|
||||
OpenStreetMap = 0, Mapbox = 1, Custom = 2
|
||||
OpenStreetMap = 1, Mapbox = 2, Custom = 3
|
||||
}
|
||||
|
||||
@SubConfigClass()
|
||||
export class AutoCompleteConfig {
|
||||
@ConfigProperty()
|
||||
enabled = true;
|
||||
@ConfigProperty()
|
||||
maxItemsPerCategory = 5;
|
||||
@ConfigProperty()
|
||||
enabled: boolean = true;
|
||||
@ConfigProperty({type: 'unsignedInt'})
|
||||
maxItemsPerCategory: number = 5;
|
||||
@ConfigProperty({type: 'unsignedInt'})
|
||||
cacheTimeout: number = 1000 * 60 * 60;
|
||||
}
|
||||
|
||||
@ -28,11 +28,11 @@ export module ClientConfig {
|
||||
enabled: boolean = true;
|
||||
@ConfigProperty()
|
||||
instantSearchEnabled: boolean = true;
|
||||
@ConfigProperty()
|
||||
@ConfigProperty({type: 'unsignedInt'})
|
||||
InstantSearchTimeout: number = 3000;
|
||||
@ConfigProperty()
|
||||
@ConfigProperty({type: 'unsignedInt'})
|
||||
instantSearchCacheTimeout: number = 1000 * 60 * 60;
|
||||
@ConfigProperty()
|
||||
@ConfigProperty({type: 'unsignedInt'})
|
||||
searchCacheTimeout: number = 1000 * 60 * 60;
|
||||
@ConfigProperty()
|
||||
AutoComplete: AutoCompleteConfig = new AutoCompleteConfig();
|
||||
@ -66,7 +66,7 @@ export module ClientConfig {
|
||||
enabled: boolean = true;
|
||||
@ConfigProperty()
|
||||
useImageMarkers: boolean = true;
|
||||
@ConfigProperty()
|
||||
@ConfigProperty({type: MapProviders})
|
||||
mapProvider: MapProviders = MapProviders.OpenStreetMap;
|
||||
@ConfigProperty()
|
||||
mapboxAccessToken: string = '';
|
||||
@ -76,11 +76,11 @@ export module ClientConfig {
|
||||
|
||||
@SubConfigClass()
|
||||
export class ThumbnailConfig {
|
||||
@ConfigProperty()
|
||||
@ConfigProperty({type: 'unsignedInt', max: 100})
|
||||
iconSize: number = 45;
|
||||
@ConfigProperty()
|
||||
@ConfigProperty({type: 'unsignedInt'})
|
||||
personThumbnailSize: number = 200;
|
||||
@ConfigProperty({arrayType: Number})
|
||||
@ConfigProperty({arrayType: 'unsignedInt'})
|
||||
thumbnailSizes: number[] = [240, 480];
|
||||
@ConfigProperty({volatile: true})
|
||||
concurrentThumbnailGenerations: number = 1;
|
||||
@ -184,7 +184,7 @@ export module ClientConfig {
|
||||
authenticationRequired: boolean = true;
|
||||
@ConfigProperty({type: UserRoles})
|
||||
unAuthenticatedUserRole: UserRoles = UserRoles.Admin;
|
||||
@ConfigProperty({arrayType: String, volatile: true})
|
||||
@ConfigProperty({arrayType: 'string', volatile: true})
|
||||
languages: string[];
|
||||
@ConfigProperty()
|
||||
Media: MediaConfig = new MediaConfig();
|
||||
|
@ -92,6 +92,7 @@ import {BackendtextService} from './model/backendtext.service';
|
||||
import {JobButtonComponent} from './ui/settings/jobs/button/job-button.settings.component';
|
||||
import {ErrorInterceptor} from './model/network/helper/error.interceptor';
|
||||
import {CSRFInterceptor} from './model/network/helper/csrf.interceptor';
|
||||
import {SettingsEntryComponent} from './ui/settings/_abstract/settings-entry/settings-entry.component';
|
||||
|
||||
|
||||
@Injectable()
|
||||
@ -187,6 +188,7 @@ export function translationsFactory(locale: string) {
|
||||
DuplicateComponent,
|
||||
DuplicatesPhotoComponent,
|
||||
// Settings
|
||||
SettingsEntryComponent,
|
||||
UserMangerSettingsComponent,
|
||||
DatabaseSettingsComponent,
|
||||
MapSettingsComponent,
|
||||
@ -218,6 +220,7 @@ export function translationsFactory(locale: string) {
|
||||
{provide: HTTP_INTERCEPTORS, useClass: ErrorInterceptor, multi: true},
|
||||
{provide: UrlSerializer, useClass: CustomUrlSerializer},
|
||||
{provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig},
|
||||
StringifySortingMethod,
|
||||
NetworkService,
|
||||
ShareService,
|
||||
UserService,
|
||||
|
@ -31,13 +31,15 @@
|
||||
id="simplifiedMode"
|
||||
class="switch"
|
||||
name="simplifiedMode"
|
||||
[switch-off-color]="'warning'"
|
||||
[switch-on-color]="'primary'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Advanced"
|
||||
[switch-on-text]="text.Simplified"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
switch-off-color="warning"
|
||||
switch-on-color="primary"
|
||||
switch-inverse="true"
|
||||
switch-off-text="Advanced"
|
||||
switch-on-text="Simplified"
|
||||
i18n-switch-off-text
|
||||
i18n-switch-on-text
|
||||
switch-handle-width="100"
|
||||
switch-label-width="20"
|
||||
[(ngModel)]="simplifiedMode">
|
||||
</bSwitch>
|
||||
</div>
|
||||
|
@ -17,15 +17,11 @@ import {formatDate} from '@angular/common';
|
||||
})
|
||||
export class AdminComponent implements OnInit, AfterViewInit {
|
||||
simplifiedMode = true;
|
||||
text = {
|
||||
Advanced: 'Advanced',
|
||||
Simplified: 'Simplified'
|
||||
};
|
||||
appVersion = Config.Client.appVersion;
|
||||
versionExtra = '';
|
||||
upTime = Config.Client.upTime;
|
||||
@ViewChildren('setting') settingsComponents: QueryList<ISettingsComponent>;
|
||||
@ViewChildren('setting', {read: ElementRef}) settingsComponents2: QueryList<ElementRef>;
|
||||
@ViewChildren('setting', {read: ElementRef}) settingsComponentsElemRef: QueryList<ElementRef>;
|
||||
contents: ISettingsComponent[] = [];
|
||||
|
||||
constructor(private _authService: AuthenticationService,
|
||||
@ -33,8 +29,6 @@ export class AdminComponent implements OnInit, AfterViewInit {
|
||||
public notificationService: NotificationService,
|
||||
@Inject(LOCALE_ID) private locale: string,
|
||||
public i18n: I18n) {
|
||||
this.text.Advanced = i18n('Advanced');
|
||||
this.text.Simplified = i18n('Simplified');
|
||||
|
||||
if (Config.Client.buildTime) {
|
||||
this.versionExtra = i18n('Built at') + ': ' + formatDate(Config.Client.buildTime, 'medium', locale);
|
||||
@ -50,7 +44,7 @@ export class AdminComponent implements OnInit, AfterViewInit {
|
||||
}
|
||||
|
||||
scrollTo(i: number) {
|
||||
PageHelper.ScrollY = this.settingsComponents2.toArray()[i].nativeElement.getBoundingClientRect().top +
|
||||
PageHelper.ScrollY = this.settingsComponentsElemRef.toArray()[i].nativeElement.getBoundingClientRect().top +
|
||||
PageHelper.ScrollY;
|
||||
}
|
||||
|
||||
|
@ -16,3 +16,13 @@
|
||||
margin-bottom: -4px;
|
||||
|
||||
}
|
||||
|
||||
.changed-settings input {
|
||||
border-color: #007bff;
|
||||
border-width: 1.5px;
|
||||
}
|
||||
|
||||
.changed-settings label {
|
||||
color: #007bff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
@ -10,8 +10,29 @@ import {I18n} from '@ngx-translate/i18n-polyfill';
|
||||
import {Subscription} from 'rxjs';
|
||||
import {ISettingsComponent} from './ISettingsComponent';
|
||||
import {WebConfig} from '../../../../../common/config/private/WebConfig';
|
||||
import {FormControl} from '@angular/forms';
|
||||
|
||||
interface ConfigState {
|
||||
value: any;
|
||||
original: any;
|
||||
default: any;
|
||||
readonly: any;
|
||||
onChange: any;
|
||||
isEnumType: boolean;
|
||||
isConfigType: boolean;
|
||||
}
|
||||
|
||||
interface RecursiveState extends ConfigState {
|
||||
value: any;
|
||||
original: any;
|
||||
default: any;
|
||||
readonly: any;
|
||||
onChange: any;
|
||||
isEnumType: any;
|
||||
isConfigType: any;
|
||||
|
||||
[key: string]: RecursiveState;
|
||||
}
|
||||
|
||||
export abstract class SettingsComponent<T extends { [key: string]: any }, S extends AbstractSettingsService<T> = AbstractSettingsService<T>>
|
||||
implements OnInit, OnDestroy, OnChanges, ISettingsComponent {
|
||||
@ -20,7 +41,8 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
|
||||
public simplifiedMode = true;
|
||||
|
||||
@ViewChild('settingsForm', {static: true})
|
||||
form: HTMLFormElement;
|
||||
form: FormControl;
|
||||
|
||||
|
||||
@Output()
|
||||
hasAvailableSettings = true;
|
||||
@ -28,14 +50,9 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
|
||||
public inProgress = false;
|
||||
public error: string = null;
|
||||
public changed = false;
|
||||
public settings: T = <T>{};
|
||||
public original: T = <T>{};
|
||||
text = {
|
||||
Enabled: 'Enabled',
|
||||
Disabled: 'Disabled',
|
||||
Low: 'Low',
|
||||
High: 'High'
|
||||
};
|
||||
public states: RecursiveState = <any>{};
|
||||
|
||||
|
||||
private _subscription: Subscription = null;
|
||||
private readonly _settingsSubscription: Subscription = null;
|
||||
|
||||
@ -50,10 +67,6 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
|
||||
this._settingsSubscription = this._settingsService.Settings.subscribe(this.onNewSettings);
|
||||
this.onNewSettings(this._settingsService._settingsService.settings.value);
|
||||
}
|
||||
this.text.Enabled = i18n('Enabled');
|
||||
this.text.Disabled = i18n('Disabled');
|
||||
this.text.Low = i18n('Low');
|
||||
this.text.High = i18n('High');
|
||||
}
|
||||
|
||||
|
||||
@ -70,8 +83,22 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
|
||||
}
|
||||
|
||||
onNewSettings = (s: WebConfig) => {
|
||||
this.settings = Utils.clone(this.sliceFN(s));
|
||||
this.original = Utils.clone(this.settings);
|
||||
|
||||
this.states = Utils.clone(<any>this.sliceFN(s.State));
|
||||
const addOriginal = (obj: any) => {
|
||||
for (const k of Object.keys(obj)) {
|
||||
if (typeof obj[k].value === 'undefined') {
|
||||
if (typeof obj[k] === 'object') {
|
||||
addOriginal(obj[k]);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
obj[k].original = Utils.clone(obj[k].value);
|
||||
obj[k].onChange = this.onOptionChange;
|
||||
}
|
||||
};
|
||||
addOriginal(this.states);
|
||||
this.ngOnChanges();
|
||||
};
|
||||
|
||||
@ -104,12 +131,32 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
|
||||
return true;
|
||||
}
|
||||
|
||||
public testSettingChanges() {
|
||||
// TODO: fix after this issue is fixed: https://github.com/angular/angular/issues/24818
|
||||
onOptionChange = () => {
|
||||
setTimeout(() => {
|
||||
this.changed = !this.settingsSame(this.settings, this.original);
|
||||
}, 0);
|
||||
const settingsSame = (state: RecursiveState): boolean => {
|
||||
if (typeof state === 'undefined') {
|
||||
return true;
|
||||
}
|
||||
if (typeof state.original === 'object') {
|
||||
return Utils.equalsFilter(state.original, state.value);
|
||||
}
|
||||
if (typeof state.original !== 'undefined') {
|
||||
return state.value === state.original;
|
||||
}
|
||||
const keys = Object.keys(state);
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
if (settingsSame(state[key]) === false) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
this.changed = !settingsSame(this.states);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
ngOnInit() {
|
||||
if (!this._authService.isAuthenticated() ||
|
||||
@ -121,9 +168,8 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
|
||||
|
||||
// TODO: fix after this issue is fixed: https://github.com/angular/angular/issues/24818
|
||||
this._subscription = this.form.valueChanges.subscribe(() => {
|
||||
this.testSettingChanges();
|
||||
this.onOptionChange();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
ngOnChanges(): void {
|
||||
@ -146,11 +192,28 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
|
||||
this.getSettings();
|
||||
}
|
||||
|
||||
stateToSettings(): T {
|
||||
const ret: T = <any>{};
|
||||
|
||||
const add = (obj: any, to: any): void => {
|
||||
for (const key of Object.keys(obj)) {
|
||||
to[key] = {};
|
||||
if (obj[key].isConfigType) {
|
||||
return add(obj[key], to[key]);
|
||||
}
|
||||
to[key] = obj[key].value;
|
||||
}
|
||||
};
|
||||
add(this.states, ret);
|
||||
return ret;
|
||||
|
||||
}
|
||||
|
||||
public async save() {
|
||||
this.inProgress = true;
|
||||
this.error = '';
|
||||
try {
|
||||
await this._settingsService.updateSettings(this.settings);
|
||||
await this._settingsService.updateSettings(this.stateToSettings());
|
||||
await this.getSettings();
|
||||
this.notification.success(this.Name + ' ' + this.i18n('settings saved'), this.i18n('Success'));
|
||||
this.inProgress = false;
|
||||
@ -166,11 +229,11 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
private async getSettings() {
|
||||
await this._settingsService.getSettings();
|
||||
this.changed = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -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>
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -7,91 +7,82 @@
|
||||
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
|
||||
|
||||
|
||||
<div class="form-group row" [hidden]="simplifiedMode">
|
||||
<label class="col-md-2 control-label" for="applicationTitle" i18n>Page title</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" class="form-control" placeholder="Pigallery 2"
|
||||
id="applicationTitle"
|
||||
[(ngModel)]="settings.applicationTitle"
|
||||
name="applicationTitle" required>
|
||||
</div>
|
||||
</div>
|
||||
<app-settings-entry
|
||||
name="Page title"
|
||||
[ngModel]="states.applicationTitle"
|
||||
i18n-name
|
||||
required="true"
|
||||
[simplifiedMode]="simplifiedMode">
|
||||
</app-settings-entry>
|
||||
|
||||
<div class="form-group row" [hidden]="simplifiedMode">
|
||||
<label class="col-md-2 control-label" for="host" i18n>Host</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" class="form-control" placeholder="0.0.0.0"
|
||||
id="host"
|
||||
[(ngModel)]="settings.host"
|
||||
name="host" required>
|
||||
<small class="form-text text-muted" i18n>Server will accept connections from this IPv6 or IPv4 address.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row" [hidden]="simplifiedMode">
|
||||
<label class="col-md-2 control-label" for="port" i18n>Port</label>
|
||||
<div class="col-md-10">
|
||||
<input type="number" class="form-control" placeholder="80"
|
||||
id="port"
|
||||
min="0"
|
||||
step="1"
|
||||
max="65535"
|
||||
[(ngModel)]="settings.port"
|
||||
name="port" required>
|
||||
<small class="form-text text-muted" i18n>Port number. Port 80 is usually what you need.</small>
|
||||
</div>
|
||||
</div>
|
||||
<app-settings-entry
|
||||
name="Host"
|
||||
[ngModel]="states.host"
|
||||
description="Server will accept connections from this IPv6 or IPv4 address."
|
||||
i18n-description i18n-name
|
||||
placeholder="0.0.0.0"
|
||||
required="true"
|
||||
[simplifiedMode]="simplifiedMode">
|
||||
</app-settings-entry>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="imagesFolder" i18n>Images folder</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" class="form-control" placeholder="path"
|
||||
id="imagesFolder"
|
||||
[(ngModel)]="settings.imagesFolder"
|
||||
name="imagesFolder" required>
|
||||
<small class="form-text text-muted" i18n>Images are loaded from this folder (read permission required)</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="tempFolder" i18n>Temp folder</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" class="form-control" placeholder="path"
|
||||
id="tempFolder"
|
||||
[(ngModel)]="settings.tempFolder"
|
||||
name="tempFolder" required>
|
||||
<small class="form-text text-muted" i18n>Thumbnails, coverted photos, videos will be stored here (write permission required)</small>
|
||||
</div>
|
||||
</div>
|
||||
<app-settings-entry
|
||||
name="Port"
|
||||
description="Port number. Port 80 is usually what you need."
|
||||
i18n-description i18n-name
|
||||
[ngModel]="states.port"
|
||||
required="true"
|
||||
[simplifiedMode]="simplifiedMode">
|
||||
</app-settings-entry>
|
||||
|
||||
<div class="form-group row" [hidden]="simplifiedMode">
|
||||
<label class="col-md-2 control-label" for="publicUrl" i18n>Page public url</label>
|
||||
<div class="col-md-10">
|
||||
<input type="url" class="form-control" placeholder="{{urlPlaceholder}}"
|
||||
id="publicUrl"
|
||||
[(ngModel)]="settings.publicUrl"
|
||||
|
||||
<app-settings-entry
|
||||
name="Images folder"
|
||||
description="Images are loaded from this folder (read permission required)"
|
||||
placeholder="path"
|
||||
i18n-description i18n-name
|
||||
required
|
||||
[ngModel]="states.imagesFolder">
|
||||
</app-settings-entry>
|
||||
|
||||
|
||||
<app-settings-entry
|
||||
name="Temp folder"
|
||||
description="Thumbnails, converted photos, videos will be stored here (write
|
||||
permission required)"
|
||||
placeholder="path"
|
||||
i18n-description i18n-name
|
||||
required="true"
|
||||
[ngModel]="states.tempFolder">
|
||||
</app-settings-entry>
|
||||
|
||||
|
||||
<app-settings-entry
|
||||
name="Page public url"
|
||||
[(ngModel)]="states.publicUrl"
|
||||
(change)="onUrlChanged()"
|
||||
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>
|
||||
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>
|
||||
|
||||
<div class="form-group row" [hidden]="simplifiedMode">
|
||||
<label class="col-md-2 control-label" for="urlBase" i18n>Url Base</label>
|
||||
<div class="col-md-10">
|
||||
<input type="url" class="form-control" placeholder="/myGallery"
|
||||
id="urlBase"
|
||||
[(ngModel)]="settings.urlBase"
|
||||
|
||||
<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()"
|
||||
name="urlBase">
|
||||
<small class="form-text text-muted" i18n>If you access the gallery under a sub url (like:
|
||||
http://mydomain.com/myGallery), set it here. If not working you might miss the '/' from the beginning of the
|
||||
url.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
[simplifiedMode]="simplifiedMode">
|
||||
</app-settings-entry>
|
||||
|
||||
|
||||
<div *ngIf="urlError===true" class="alert alert-warning" role="alert" i18n>
|
||||
The public url and the url base are not matching. Some of the functionality might not work.
|
||||
|
@ -46,7 +46,7 @@ export class BasicSettingsComponent extends SettingsComponent<BasicConfigDTO> {
|
||||
}
|
||||
|
||||
calcBaseUrl(): string {
|
||||
const url = this.settings.publicUrl.replace(new RegExp('\\\\', 'g'), '/')
|
||||
const url = this.states.publicUrl.value.replace(new RegExp('\\\\', 'g'), '/')
|
||||
.replace(new RegExp('http://', 'g'), '')
|
||||
.replace(new RegExp('https://', 'g'), '');
|
||||
if (url.indexOf('/') !== -1) {
|
||||
@ -57,23 +57,24 @@ export class BasicSettingsComponent extends SettingsComponent<BasicConfigDTO> {
|
||||
}
|
||||
|
||||
checkUrlError() {
|
||||
this.urlError = this.settings.urlBase !== this.calcBaseUrl();
|
||||
this.urlError = this.states.urlBase.value !== this.calcBaseUrl();
|
||||
}
|
||||
|
||||
onUrlChanged() {
|
||||
console.log('called');
|
||||
if (this.urlBaseChanged === false) {
|
||||
this.settings.urlBase = this.calcBaseUrl();
|
||||
this.states.urlBase.value = this.calcBaseUrl();
|
||||
} else {
|
||||
this.checkUrlError();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
onUrlBaseChanged() {
|
||||
onUrlBaseChanged = () => {
|
||||
this.urlBaseChanged = true;
|
||||
|
||||
this.checkUrlError();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
@ -6,71 +6,68 @@
|
||||
<div class="card-body">
|
||||
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
|
||||
|
||||
|
||||
<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"
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</app-settings-entry>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="dbFolder" i18n>Database folder</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" class="form-control" placeholder="db"
|
||||
[(ngModel)]="settings.dbFolder" id="dbFolder" name="dbFolder" required>
|
||||
<small class="form-text text-muted" i18n>
|
||||
All file-based data will be stored here (sqlite database, user database in case of memory db, job history
|
||||
data)
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-container *ngIf="settings.type == DatabaseType.mysql">
|
||||
<app-settings-entry
|
||||
name="Database folder"
|
||||
description="All file-based data will be stored here (sqlite database, user database in case of memory db, job history data)"
|
||||
[ngModel]="states.dbFolder"
|
||||
i18n-name i18n-description
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="mysql_host" i18n>Host</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" class="form-control" placeholder="localhost"
|
||||
[(ngModel)]="settings.mysql.host" id="mysql_host" name="mysql_host" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="mysql_port" i18n>Port</label>
|
||||
<div class="col-md-10">
|
||||
<input type="number" class="form-control" placeholder="3306" min="0" max="65535"
|
||||
[(ngModel)]="settings.mysql.port" id="mysql_port" name="mysql_port" required>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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 *ngIf="states.type.value == DatabaseType.mysql">
|
||||
|
||||
|
||||
<app-settings-entry
|
||||
name="Host"
|
||||
[ngModel]="states.mysql.host"
|
||||
i18n-name
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
<app-settings-entry
|
||||
name="Port"
|
||||
[ngModel]="states.mysql.port"
|
||||
i18n-name
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
|
||||
<app-settings-entry
|
||||
name="Database"
|
||||
[ngModel]="states.mysql.database"
|
||||
i18n-name
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
<app-settings-entry
|
||||
name="Username"
|
||||
[ngModel]="states.mysql.username"
|
||||
placeholder="username"
|
||||
i18n-name
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
|
||||
<app-settings-entry
|
||||
name="Password"
|
||||
[ngModel]="states.mysql.password"
|
||||
placeholder="password"
|
||||
i18n-name
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
</ng-container>
|
||||
|
||||
|
||||
|
@ -1,62 +1,49 @@
|
||||
<form #settingsForm="ngForm">
|
||||
<div class="card mb-4"
|
||||
[ngClass]="settings.enabled && !_settingsService.isSupported()?'panel-warning':''">
|
||||
[ngClass]="states.enabled.value && !_settingsService.isSupported()?'panel-warning':''">
|
||||
<h5 class="card-header">
|
||||
{{Name}}
|
||||
<div class="switch-wrapper">
|
||||
<div class="switch-wrapper"
|
||||
[class.changed-settings]="states.enabled.value != states.enabled.default">
|
||||
<bSwitch
|
||||
class="switch"
|
||||
name="enabled"
|
||||
[switch-on-color]="'success'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-disabled]="inProgress || (!settings.enabled && !_settingsService.isSupported())"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.enabled">
|
||||
switch-on-color="success"
|
||||
switch-inverse="true"
|
||||
switch-off-text="Disabled"
|
||||
switch-on-text="Enabled"
|
||||
i18n-switch-off-text
|
||||
i18n-switch-on-text
|
||||
[switch-disabled]="inProgress || (!states.enabled.value && !_settingsService.isSupported())"
|
||||
switch-handle-width="100"
|
||||
switch-label-width="20"
|
||||
[(ngModel)]="states.enabled.value">
|
||||
</bSwitch>
|
||||
</div>
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
|
||||
|
||||
<ng-container *ngIf="settings.enabled || _settingsService.isSupported()">
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="autocompleteEnabled" i18n>Override keywords</label>
|
||||
<div class="col-md-10">
|
||||
<bSwitch
|
||||
id="autocompleteEnabled"
|
||||
class="switch"
|
||||
name="autocompleteEnabled"
|
||||
[switch-on-color]="'primary'"
|
||||
[switch-disabled]="!settings.enabled"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.keywordsToPersons">
|
||||
</bSwitch>
|
||||
<small class="form-text text-muted" i18n>If a photo has the same face (person) name and keyword, the app removes the duplicate, keeping the face only.</small>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="states.enabled.value || _settingsService.isSupported()">
|
||||
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="writeAccessMinRole" i18n>Face starring right</label>
|
||||
<div class="col-md-10">
|
||||
<select class="form-control" [(ngModel)]="settings.writeAccessMinRole" name="writeAccessMinRole" id="writeAccessMinRole" required>
|
||||
<option *ngFor="let repository of userRoles" [value]="repository.key">{{repository.value}}
|
||||
</option>
|
||||
</select>
|
||||
<small class="form-text text-muted" i18n>Required minimum right to start (favourite) a face.</small>
|
||||
</div>
|
||||
</div>
|
||||
<app-settings-entry
|
||||
name="Override keywords"
|
||||
[ngModel]="states.keywordsToPersons"
|
||||
description="If a photo has the same face (person) name and keyword, the app removes the duplicate, keeping the face only."
|
||||
i18n-name i18n-description>
|
||||
</app-settings-entry>
|
||||
|
||||
<app-settings-entry
|
||||
name="Face starring right"
|
||||
[ngModel]="states.writeAccessMinRole"
|
||||
description="Required minimum right to start (favourite) a face."
|
||||
i18n-name i18n-description>
|
||||
</app-settings-entry>
|
||||
|
||||
</ng-container>
|
||||
<div class="panel-info" *ngIf="(!settings.enabled && !_settingsService.isSupported())" i18n>
|
||||
|
||||
<div class="panel-info" *ngIf="(!states.enabled.value && !_settingsService.isSupported())" i18n>
|
||||
Faces are not supported with these settings.
|
||||
</div>
|
||||
<button class="btn btn-success float-right"
|
||||
|
@ -7,55 +7,38 @@
|
||||
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
|
||||
|
||||
<ng-container *ngIf="!simplifiedMode">
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="cachedFolderTimeout" i18n>Index cache timeout [ms]</label>
|
||||
<div class="col-md-10">
|
||||
<input type="number" class="form-control" placeholder="36000"
|
||||
id="cachedFolderTimeout"
|
||||
min="0"
|
||||
step="1"
|
||||
[(ngModel)]="settings.cachedFolderTimeout"
|
||||
name="cachedFolderTimeout" required>
|
||||
<small class="form-text text-muted" i18n>If there was no indexing in this time, it reindexes. (skipped if
|
||||
indexes are in DB and sensitivity is low)
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="folderPreviewSize" i18n>Sub folder preview size</label>
|
||||
<div class="col-md-10">
|
||||
<input type="number" class="form-control" placeholder="1"
|
||||
id="folderPreviewSize"
|
||||
min="0"
|
||||
step="1"
|
||||
[(ngModel)]="settings.folderPreviewSize"
|
||||
name="folderPreviewSize" required>
|
||||
<small class="form-text text-muted" i18n>Reads this many photos from sub folders</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="reIndexingSensitivity" i18n>Folder reindexing sensitivity</label>
|
||||
<div class="col-md-10">
|
||||
<select id="reIndexingSensitivity" class="form-control" [(ngModel)]="settings.reIndexingSensitivity"
|
||||
name="reIndexingSensitivity" required>
|
||||
<option *ngFor="let type of types" [ngValue]="type.key">{{type.value}}
|
||||
</option>
|
||||
</select>
|
||||
<small
|
||||
class="form-text text-muted"
|
||||
i18n>Set the reindexing sensitivity. High value check the folders for change more often.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<app-settings-entry
|
||||
name="Index cache timeout [ms]"
|
||||
description="If there was no indexing in this time, it reindexes. (skipped if indexes are in DB and sensitivity is low)"
|
||||
i18n-description i18n-name
|
||||
[ngModel]="states.cachedFolderTimeout"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
<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>
|
||||
<app-settings-entry
|
||||
name="Sub folder preview size"
|
||||
description="Reads this many photos from sub folders."
|
||||
i18n-description i18n-name
|
||||
[ngModel]="states.folderPreviewSize"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
|
||||
<app-settings-entry
|
||||
name="Folder reindexing sensitivity"
|
||||
[ngModel]="states.reIndexingSensitivity"
|
||||
description="Set the reindexing sensitivity. High value check the folders for change more often."
|
||||
i18n-description i18n-name
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
<app-settings-entry
|
||||
name="Exclude Folder List"
|
||||
i18n-name
|
||||
placeholder="/media/images/family/private;private;family/private"
|
||||
[ngModel]="states.excludeFolderList"
|
||||
required="true">
|
||||
<small class="form-text text-muted">
|
||||
<ng-container i18n>Folders to exclude from indexing</ng-container>
|
||||
<br/>
|
||||
@ -65,16 +48,14 @@
|
||||
'/', any folder with this name will be excluded.
|
||||
</ng-container>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</app-settings-entry>
|
||||
|
||||
<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>
|
||||
<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/>
|
||||
@ -83,8 +64,9 @@
|
||||
indexing.
|
||||
</ng-container>
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</app-settings-entry>
|
||||
|
||||
|
||||
|
||||
|
||||
<button class="btn btn-success float-right"
|
||||
|
@ -49,21 +49,6 @@ export class IndexingSettingsComponent extends SettingsComponent<ServerConfig.In
|
||||
return this.jobsService.progress.value[JobDTO.getHashName(DefaultsJobs[DefaultsJobs.Indexing])];
|
||||
}
|
||||
|
||||
get excludeFolderList(): string {
|
||||
return this.settings.excludeFolderList.join(';');
|
||||
}
|
||||
|
||||
set excludeFolderList(value: string) {
|
||||
this.settings.excludeFolderList = value.split(';');
|
||||
}
|
||||
|
||||
get excludeFileList(): string {
|
||||
return this.settings.excludeFileList.join(';');
|
||||
}
|
||||
|
||||
set excludeFileList(value: string) {
|
||||
this.settings.excludeFileList = value.split(';');
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
super.ngOnDestroy();
|
||||
|
@ -83,7 +83,7 @@
|
||||
<div class="col-md-10">
|
||||
<select class="form-control" [(ngModel)]="schedule.trigger.afterScheduleName"
|
||||
[name]="'triggerAfter'+i" required>
|
||||
<ng-container *ngFor="let sch of settings.scheduled">
|
||||
<ng-container *ngFor="let sch of states.scheduled.value">
|
||||
<option *ngIf="sch.name !== schedule.name"
|
||||
[ngValue]="sch.name">{{sch.name}}
|
||||
</option>
|
||||
@ -101,7 +101,7 @@
|
||||
<div class="col-md-10">
|
||||
<app-timestamp-datepicker
|
||||
[name]="'triggerTime'+i"
|
||||
(timestampChange)="testSettingChanges()"
|
||||
(timestampChange)="onOptionChange()"
|
||||
[(timestamp)]="schedule.trigger.time"></app-timestamp-datepicker>
|
||||
</div>
|
||||
</div>
|
||||
@ -121,7 +121,7 @@
|
||||
</select>
|
||||
<app-timestamp-timepicker
|
||||
[name]="'atTime'+i"
|
||||
(timestampChange)="testSettingChanges()"
|
||||
(timestampChange)="onOptionChange()"
|
||||
[(timestamp)]="schedule.trigger.atTime"></app-timestamp-timepicker>
|
||||
</div>
|
||||
</div>
|
||||
@ -132,12 +132,13 @@
|
||||
class="switch"
|
||||
[name]="'allowParallelRun'+'_'+i"
|
||||
[id]="'allowParallelRun'+'_'+i"
|
||||
[switch-on-color]="'primary'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
switch-on-color="primary"
|
||||
switch-inverse="true"
|
||||
switch-off-text="Disabled"
|
||||
switch-on-text="Enabled"
|
||||
i18n-switch-off-text i18n-switch-on-text
|
||||
switch-handle-width="100"
|
||||
switch-label-width="20"
|
||||
[(ngModel)]="schedule.allowParallelRun">
|
||||
</bSwitch>
|
||||
<small class="form-text text-muted"
|
||||
@ -164,12 +165,13 @@
|
||||
class="switch"
|
||||
[name]="configEntry.id+'_'+i"
|
||||
[id]="configEntry.id+'_'+i"
|
||||
[switch-on-color]="'primary'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
switch-on-color="primary"
|
||||
switch-inverse="true"
|
||||
switch-off-text="Disabled"
|
||||
switch-on-text="Enabled"
|
||||
i18n-switch-off-text i18n-switch-on-text
|
||||
switch-handle-width="100"
|
||||
switch-label-width="20"
|
||||
[(ngModel)]="schedule.config[configEntry.id]">
|
||||
</bSwitch>
|
||||
</ng-container>
|
||||
|
@ -100,7 +100,7 @@ export class JobsSettingsComponent extends SettingsComponent<ServerConfig.JobCon
|
||||
}
|
||||
|
||||
remove(schedule: JobScheduleDTO) {
|
||||
this.settings.scheduled.splice(this.settings.scheduled.indexOf(schedule), 1);
|
||||
this.states.scheduled.value.splice(this.states.scheduled.value.indexOf(schedule), 1);
|
||||
}
|
||||
|
||||
jobTypeChanged(schedule: JobScheduleDTO) {
|
||||
@ -163,17 +163,17 @@ export class JobsSettingsComponent extends SettingsComponent<ServerConfig.JobCon
|
||||
return curr && curr.trigger.type === JobTriggerType.after && prev && prev.name === curr.trigger.afterScheduleName;
|
||||
}
|
||||
|
||||
public sortedSchedules() {
|
||||
return this.settings.scheduled.slice().sort((a, b) => {
|
||||
return this.getNextRunningDate(a, this.settings.scheduled) - this.getNextRunningDate(b, this.settings.scheduled);
|
||||
public sortedSchedules(): JobScheduleDTO[] {
|
||||
return this.states.scheduled.value.slice().sort((a: JobScheduleDTO, b: JobScheduleDTO) => {
|
||||
return this.getNextRunningDate(a, this.states.scheduled.value) - this.getNextRunningDate(b, this.states.scheduled.value);
|
||||
});
|
||||
}
|
||||
|
||||
addNewJob() {
|
||||
const jobName = this.newSchedule.jobName;
|
||||
const count = this.settings.scheduled.filter(s => s.jobName === jobName).length;
|
||||
const count = this.states.scheduled.value.filter((s: JobScheduleDTO) => s.jobName === jobName).length;
|
||||
this.newSchedule.name = count === 0 ? jobName : this.backendTextService.getJobName(jobName) + ' ' + (count + 1);
|
||||
this.settings.scheduled.push(this.newSchedule);
|
||||
this.states.scheduled.value.push(this.newSchedule);
|
||||
this.jobModal.hide();
|
||||
}
|
||||
|
||||
|
@ -6,56 +6,45 @@
|
||||
<bSwitch
|
||||
class="switch"
|
||||
name="enabled"
|
||||
[switch-on-color]="'success'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
switch-on-color="success"
|
||||
switch-inverse="true"
|
||||
switch-off-text="Disabled"
|
||||
switch-on-text="Enabled"
|
||||
i18n-switch-off-text
|
||||
i18n-switch-on-text
|
||||
[switch-disabled]="inProgress"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.enabled">
|
||||
switch-handle-width="100"
|
||||
switch-label-width="20"
|
||||
[(ngModel)]="states.enabled.value">
|
||||
</bSwitch>
|
||||
</div>
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
|
||||
<ng-container *ngIf="settings.enabled">
|
||||
|
||||
<div class="form-group row" [hidden]="simplifiedMode">
|
||||
<label class="col-md-2 control-label" for="enableOnScrollRendering" i18n>Use image markers</label>
|
||||
<div class="col-md-10">
|
||||
<bSwitch
|
||||
id="enableOnScrollRendering"
|
||||
class="switch"
|
||||
name="enableOnScrollRendering"
|
||||
[switch-on-color]="'primary'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.useImageMarkers">
|
||||
</bSwitch>
|
||||
<small class="form-text text-muted" i18n>
|
||||
Map will use thumbnail images as markers instead of the default pin.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="states.enabled.value">
|
||||
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="mapProvider" i18n>Map provider</label>
|
||||
<div class="col-md-10">
|
||||
<select name="mapProvider" id="mapProvider"
|
||||
[disabled]="!settings.enabled"
|
||||
class="form-control" [(ngModel)]="settings.mapProvider" required>
|
||||
<option *ngFor="let type of mapProviders" [ngValue]="type.key">{{type.value}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container custom-layer-container" *ngIf="settings.mapProvider === MapProviders.Custom">
|
||||
|
||||
<app-settings-entry
|
||||
name="Use image markers"
|
||||
description="Map will use thumbnail images as markers instead of the default pin."
|
||||
i18n-name i18n-description=""
|
||||
[ngModel]="states.useImageMarkers"
|
||||
[simplifiedMode]="simplifiedMode">
|
||||
</app-settings-entry>
|
||||
|
||||
|
||||
|
||||
<app-settings-entry
|
||||
name="Map provider"
|
||||
i18n-name
|
||||
[ngModel]="states.mapProvider"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
|
||||
<div class="container custom-layer-container" *ngIf="states.mapProvider.value === MapProviders.Custom">
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
@ -64,7 +53,7 @@
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr *ngFor="let layer of settings.customLayers; let i = index">
|
||||
<tr *ngFor="let layer of states.customLayers.value; let i = index">
|
||||
<td><input type="text" class="form-control" placeholder="Street"
|
||||
[(ngModel)]="layer.name"
|
||||
[name]="'tileName-'+i" [id]="'tileName-'+i" required></td>
|
||||
@ -74,8 +63,8 @@
|
||||
[name]="'tileUrl-'+i" [id]="'tileUrl-'+i" required>
|
||||
</td>
|
||||
<td>
|
||||
<button [disabled]="settings.customLayers.length == 1" (click)="removeLayer(layer)"
|
||||
[ngClass]="settings.customLayers.length > 1? 'btn-danger':'btn-secondary'"
|
||||
<button [disabled]="states.customLayers.value.length == 1" (click)="removeLayer(layer)"
|
||||
[ngClass]="states.customLayers.value.length > 1? 'btn-danger':'btn-secondary'"
|
||||
class="btn float-right">
|
||||
<span class="oi oi-trash" aria-hidden="true" aria-label="Delete"></span>
|
||||
</button>
|
||||
@ -95,18 +84,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row" *ngIf="settings.mapProvider === MapProviders.Mapbox">
|
||||
<label class="col-md-2 control-label" for="mapboxAccessToken" i18n>Mapbox access token</label>
|
||||
<div class="col-md-10">
|
||||
<input type="text" class="form-control" placeholder="Mapbox access token"
|
||||
[(ngModel)]="settings.mapboxAccessToken"
|
||||
name="mapboxAccessToken" id="mapboxAccessToken" required>
|
||||
<app-settings-entry
|
||||
*ngIf="states.mapProvider.value === MapProviders.Mapbox"
|
||||
name="Mapbox access token"
|
||||
i18n-name
|
||||
placeholder="Mapbox access token"
|
||||
[ngModel]="states.mapboxAccessToken"
|
||||
required="true">
|
||||
<small class="form-text text-muted">
|
||||
<ng-container i18n>MapBox needs an access token to work, create one at</ng-container>
|
||||
<a href="https://www.mapbox.com">https://www.mapbox.com</a>.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</app-settings-entry>
|
||||
|
||||
|
||||
</ng-container>
|
||||
<button class="btn btn-success float-right"
|
||||
|
@ -33,14 +33,14 @@ export class MapSettingsComponent extends SettingsComponent<ClientConfig.MapConf
|
||||
|
||||
|
||||
addNewLayer() {
|
||||
this.settings.customLayers.push({
|
||||
name: 'Layer-' + this.settings.customLayers.length,
|
||||
this.states.customLayers.value.push({
|
||||
name: 'Layer-' + this.states.customLayers.value.length,
|
||||
url: ''
|
||||
});
|
||||
}
|
||||
|
||||
removeLayer(layer: ClientConfig.MapLayers) {
|
||||
this.settings.customLayers.splice(this.settings.customLayers.indexOf(layer), 1);
|
||||
this.states.customLayers.value.splice(this.states.customLayers.value.indexOf(layer), 1);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,14 +6,16 @@
|
||||
<bSwitch
|
||||
class="switch"
|
||||
name="enabled"
|
||||
[switch-on-color]="'success'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
switch-on-color="success"
|
||||
switch-inverse="true"
|
||||
switch-off-text="Disabled"
|
||||
switch-on-text="Enabled"
|
||||
i18n-switch-off-text
|
||||
i18n-switch-on-text
|
||||
[switch-disabled]="inProgress"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.enabled">
|
||||
switch-handle-width="100"
|
||||
switch-label-width="20"
|
||||
[(ngModel)]="states.enabled.value">
|
||||
</bSwitch>
|
||||
</div>
|
||||
</h5>
|
||||
|
@ -8,161 +8,89 @@
|
||||
|
||||
|
||||
<p class="title" i18n>Threads:</p>
|
||||
<div class="form-group row" >
|
||||
<label class="col-md-2 control-label" for="enableThreading" i18n>Threading</label>
|
||||
<div class="col-md-10">
|
||||
<bSwitch
|
||||
id="enableThreading"
|
||||
class="switch"
|
||||
name="enable"
|
||||
[switch-on-color]="'primary'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.Server.enabled">
|
||||
</bSwitch>
|
||||
<small class="form-text text-muted" i18n>Runs directory scanning and thumbnail generation (only for Jimp) in a
|
||||
different thread
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row" [hidden]="simplifiedMode || settings.Server.enabled == false">
|
||||
<label class="col-md-2 control-label" for="thumbnailThreads" i18n>Thumbnail threads</label>
|
||||
<div class="col-md-10">
|
||||
<select id="thumbnailThreads" class="form-control" [(ngModel)]="settings.Server.thumbnailThreads"
|
||||
name="Server[thumbnailThreads]" required>
|
||||
<option [ngValue]="0">auto</option>
|
||||
<option *ngFor="let i of threads" [ngValue]="i">{{i}}</option>
|
||||
</select>
|
||||
<small class="form-text text-muted" i18n>Number of threads that are used to generate thumbnails. If auto, number of CPU cores -1 threads will be used.</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-settings-entry
|
||||
name="Threading"
|
||||
description="Runs directory scanning and thumbnail generation (only for Jimp) in a different thread."
|
||||
i18n-description i18n-name
|
||||
[ngModel]="states.Server.enabled"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
|
||||
<app-settings-entry
|
||||
name="Thumbnail threads"
|
||||
description="Number of threads that are used to generate thumbnails. If auto, number of CPU cores -1 threads will be used."
|
||||
i18n-description i18n-name
|
||||
[ngModel]="states.Server.thumbnailThreads"
|
||||
[options]="threads"
|
||||
[simplifiedMode]="simplifiedMode || states.Server.enabled.value == false"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
|
||||
<hr/>
|
||||
<p class="title" i18n>Misc:</p>
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="enableOnScrollThumbnailPrioritising" i18n>Scroll based thumbnail
|
||||
generation</label>
|
||||
<div class="col-md-10">
|
||||
<bSwitch
|
||||
id="enableOnScrollThumbnailPrioritising"
|
||||
class="switch"
|
||||
name="enableOnScrollThumbnailPrioritising"
|
||||
[switch-on-color]="'primary'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.Client.enableOnScrollThumbnailPrioritising">
|
||||
</bSwitch>
|
||||
<small class="form-text text-muted" i18n>Those thumbnails get higher priority that are visible on the screen
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="enableOnScrollRendering" i18n>Lazy image rendering</label>
|
||||
<div class="col-md-10">
|
||||
<bSwitch
|
||||
id="enableOnScrollRendering"
|
||||
class="switch"
|
||||
name="enableOnScrollRendering"
|
||||
[switch-on-color]="'primary'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.Client.enableOnScrollRendering">
|
||||
</bSwitch>
|
||||
<small class="form-text text-muted" i18n>Shows only the required amount of photos at once. Renders more if
|
||||
page bottom is reached
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<app-settings-entry
|
||||
name="Scroll based thumbnail generation"
|
||||
description="Those thumbnails get higher priority that are visible on the screen."
|
||||
i18n-description i18n-name
|
||||
[ngModel]="states.Client.enableOnScrollThumbnailPrioritising"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
<app-settings-entry
|
||||
name="Lazy image rendering"
|
||||
description="Shows only the required amount of photos at once. Renders more if page bottom is reached."
|
||||
i18n-description i18n-name
|
||||
[ngModel]="states.Client.enableOnScrollRendering"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="enableCache" i18n>Cache</label>
|
||||
<div class="col-md-10">
|
||||
<bSwitch
|
||||
id="enableCache"
|
||||
class="switch"
|
||||
name="enableCache"
|
||||
[switch-on-color]="'primary'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.Client.enableCache">
|
||||
</bSwitch>
|
||||
<small class="form-text text-muted" i18n>Caches directory contents and search results for better performance
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<app-settings-entry
|
||||
name="Cache"
|
||||
description="Caches directory contents and search results for better performance."
|
||||
i18n-description i18n-name
|
||||
[ngModel]="states.Client.enableCache"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
<app-settings-entry
|
||||
name="Caption first naming"
|
||||
description="Show the caption (IPTC 120) tags from the EXIF data instead of the filenames."
|
||||
i18n-description i18n-name
|
||||
[ngModel]="states.Client.captionFirstNaming"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="enableCache" i18n>Caption first naming</label>
|
||||
<div class="col-md-10">
|
||||
<bSwitch
|
||||
id="captionFirstNaming"
|
||||
class="switch"
|
||||
name="captionFirstNaming"
|
||||
[switch-on-color]="'primary'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.Client.captionFirstNaming">
|
||||
</bSwitch>
|
||||
<small class="form-text text-muted" i18n>Show the caption (IPTC 120) tags from the EXIF data instead of the filenames.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<hr/>
|
||||
<p class="title" i18n>Navigation bar:</p>
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="showItemCount" [hidden]="simplifiedMode" i18n>Show item count</label>
|
||||
<div class="col-md-10">
|
||||
<bSwitch
|
||||
id="showItemCount"
|
||||
class="switch"
|
||||
name="showItemCount"
|
||||
[switch-on-color]="'primary'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.Client.NavBar.showItemCount">
|
||||
</bSwitch>
|
||||
<small class="form-text text-muted" i18n>Show the number of items (photos) in the folder
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-settings-entry
|
||||
name="Show item count"
|
||||
description="Show the number of items (photos) in the folder."
|
||||
i18n-description i18n-name
|
||||
[ngModel]="states.Client.NavBar.showItemCount"
|
||||
[simplifiedMode]="simplifiedMode"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
|
||||
|
||||
<div class="form-group row" [hidden]="simplifiedMode">
|
||||
<label class="col-md-2 control-label" for="defaultPhotoSortingMethod" i18n>Default photo sorting method</label>
|
||||
<div class="col-md-10">
|
||||
<select id="defaultPhotoSortingMethod" class="form-control" [(ngModel)]="settings.Client.defaultPhotoSortingMethod"
|
||||
name="defaultPhotoSortingMethod" required>
|
||||
<option *ngFor="let type of types" [ngValue]="type.key">{{type.key | stringifySorting}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<app-settings-entry
|
||||
name="Default photo sorting method"
|
||||
i18n-name
|
||||
[ngModel]="states.Client.defaultPhotoSortingMethod"
|
||||
[optionMap]="sortingMap"
|
||||
[simplifiedMode]="simplifiedMode"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
|
||||
|
||||
<button class="btn btn-success float-right"
|
||||
|
@ -8,6 +8,7 @@ import {OtherConfigDTO} from '../../../../../common/entities/settings/OtherConfi
|
||||
import {I18n} from '@ngx-translate/i18n-polyfill';
|
||||
import {Utils} from '../../../../../common/Utils';
|
||||
import {SortingMethods} from '../../../../../common/entities/SortingMethods';
|
||||
import {StringifySortingMethod} from '../../../pipes/StringifySortingMethod';
|
||||
|
||||
@Component({
|
||||
selector: 'app-settings-other',
|
||||
@ -20,17 +21,24 @@ export class OtherSettingsComponent extends SettingsComponent<OtherConfigDTO> im
|
||||
|
||||
|
||||
types: { key: number; value: string }[] = [];
|
||||
threads: number[] = Utils.createRange(1, 100);
|
||||
threads: { key: number, value: string }[] = [{key: 0, value: 'auto'}].concat(Utils.createRange(1, 100)
|
||||
.map(v => ({key: v, value: '' + v})));
|
||||
sortingMap: any;
|
||||
|
||||
constructor(_authService: AuthenticationService,
|
||||
_navigation: NavigationService,
|
||||
_settingsService: OtherSettingsService,
|
||||
notification: NotificationService,
|
||||
i18n: I18n) {
|
||||
i18n: I18n,
|
||||
private formatter: StringifySortingMethod) {
|
||||
super(i18n('Other'), _authService, _navigation, _settingsService, notification, i18n, s => ({
|
||||
Server: s.Server.Threading,
|
||||
Client: s.Client.Other
|
||||
}));
|
||||
this.sortingMap = (v: { key: number; value: string }) => {
|
||||
v.value = this.formatter.transform(v.key);
|
||||
return v;
|
||||
};
|
||||
this.types = Utils.enumToArray(SortingMethods);
|
||||
this.hasAvailableSettings = !this.simplifiedMode;
|
||||
}
|
||||
|
@ -4,90 +4,64 @@
|
||||
{{Name}}</h5>
|
||||
<div class="card-body">
|
||||
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
|
||||
<div [hidden]="settings.photoProcessingLibrary!=PhotoProcessingLib.Jimp"
|
||||
<div [hidden]="states.photoProcessingLibrary.value!=PhotoProcessingLib.Jimp"
|
||||
class="alert alert-warning"
|
||||
role="alert" i18n>It is highly recommended to use hardware accelerated (sharp or gm) lib for thumbnail
|
||||
generation
|
||||
</div>
|
||||
|
||||
<div [hidden]="simplifiedMode">
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="lib" i18n>Thumbnail generation library</label>
|
||||
<div class="col-md-10">
|
||||
<select id="lib" class="form-control" [(ngModel)]="settings.photoProcessingLibrary"
|
||||
name="type" required>
|
||||
<option *ngFor="let type of libTypes" [ngValue]="type.key">{{type.value}}
|
||||
</option>
|
||||
</select>
|
||||
<small *ngIf="settings.photoProcessingLibrary==PhotoProcessingLib.sharp"
|
||||
|
||||
<app-settings-entry
|
||||
name="Thumbnail generation library"
|
||||
i18n-name
|
||||
[ngModel]="states.photoProcessingLibrary"
|
||||
[optionMap]="libTypesMap"
|
||||
[simplifiedMode]="simplifiedMode"
|
||||
required="true">
|
||||
<small *ngIf="states.photoProcessingLibrary.value==PhotoProcessingLib.sharp"
|
||||
class="form-text text-muted" i18n>Make sure that sharp node module is installed (npm install sharp).
|
||||
</small>
|
||||
<small *ngIf="settings.photoProcessingLibrary==PhotoProcessingLib.gm"
|
||||
<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>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
</div>
|
||||
<p class="title" i18n>Photo converting:</p>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="enablePhotoConverting" i18n>Converting</label>
|
||||
<div class="col-md-10">
|
||||
<bSwitch
|
||||
id="enablePhotoConverting"
|
||||
class="switch"
|
||||
name="enablePhotoConverting"
|
||||
[switch-on-color]="'primary'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.client.Converting.enabled">
|
||||
</bSwitch>
|
||||
<small class="form-text text-muted" i18n>Downsizes photos for faster preview loading. (Zooming in to the photo
|
||||
loads the original)</small>
|
||||
</div>
|
||||
</div>
|
||||
<app-settings-entry
|
||||
name="Converting"
|
||||
description="Downsizes photos for faster preview loading. (Zooming in to the photo loads the original)."
|
||||
i18n-description i18n-name
|
||||
[ngModel]="states.client.Converting.enabled"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
<div class="form-group row" [hidden]="!settings.client.Converting.enabled || simplifiedMode">
|
||||
<label class="col-md-2 control-label" for="onTheFlyConverting" i18n>On the fly converting </label>
|
||||
<div class="col-md-10">
|
||||
<bSwitch
|
||||
id="onTheFlyConverting"
|
||||
class="switch"
|
||||
name="onTheFlyConverting"
|
||||
[switch-on-color]="'primary'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.server.Converting.onTheFly">
|
||||
</bSwitch>
|
||||
<small class="form-text text-muted" i18n>Converts photos on the fly, when they are requested.</small>
|
||||
</div>
|
||||
</div>
|
||||
<app-settings-entry
|
||||
name="On the fly converting"
|
||||
description="Converts photos on the fly, when they are requested."
|
||||
i18n-description i18n-name
|
||||
[ngModel]="states.server.Converting.onTheFly"
|
||||
[simplifiedMode]="simplifiedMode"
|
||||
[disabled]="!states.client.Converting.enabled.value"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
<div class="form-group row" [hidden]="!settings.client.Converting.enabled">
|
||||
<label class="col-md-2 control-label" for="resolution" i18n>Resolution</label>
|
||||
<div class="col-md-10">
|
||||
<select id="resolution" class="form-control" [(ngModel)]="settings.server.Converting.resolution"
|
||||
name="resolution" required>
|
||||
<option *ngFor="let resolution of resolutions" [ngValue]="resolution">{{resolution}}px
|
||||
</option>
|
||||
</select>
|
||||
<small class="form-text text-muted" i18n>The shorter edge of the converted photo will be scaled down to this,
|
||||
while
|
||||
keeping the aspect ratio.</small>
|
||||
</div>
|
||||
</div>
|
||||
<app-settings-entry
|
||||
name="Resolution"
|
||||
description="The shorter edge of the converted photo will be scaled down to this, while keeping the aspect ratio."
|
||||
i18n-description i18n-name
|
||||
[ngModel]="states.server.Converting.resolution"
|
||||
[options]="resolutions"
|
||||
[disabled]="!states.client.Converting.enabled.value"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
|
||||
<button class="btn btn-success float-right"
|
||||
@ -99,7 +73,7 @@
|
||||
(click)="reset()" i18n>Reset
|
||||
</button>
|
||||
|
||||
<div [hidden]="!settings.client.Converting.enabled">
|
||||
<div [hidden]="!states.client.Converting.enabled.value">
|
||||
<app-settings-job-button class="mt-2 mt-md-0 float-left"
|
||||
[soloRun]="true"
|
||||
(error)="error=$event"
|
||||
|
@ -6,7 +6,6 @@ import {NavigationService} from '../../../model/navigation.service';
|
||||
import {NotificationService} from '../../../model/notification.service';
|
||||
import {I18n} from '@ngx-translate/i18n-polyfill';
|
||||
import {ScheduledJobsService} from '../scheduled-jobs.service';
|
||||
import {Utils} from '../../../../../common/Utils';
|
||||
import {DefaultsJobs, JobDTO} from '../../../../../common/entities/job/JobDTO';
|
||||
import {JobProgressStates} from '../../../../../common/entities/job/JobProgressDTO';
|
||||
import {ServerConfig} from '../../../../../common/config/private/PrivateConfig';
|
||||
@ -25,19 +24,11 @@ export class PhotoSettingsComponent extends SettingsComponent<{
|
||||
server: ServerConfig.PhotoConfig,
|
||||
client: ClientConfig.PhotoConfig
|
||||
}> {
|
||||
resolutions = [720, 1080, 1440, 2160, 4320];
|
||||
readonly resolutionTypes = [720, 1080, 1440, 2160, 4320];
|
||||
resolutions: { key: number, value: string }[] = [];
|
||||
PhotoProcessingLib = ServerConfig.PhotoProcessingLib;
|
||||
JobProgressStates = JobProgressStates;
|
||||
|
||||
libTypes = Utils
|
||||
.enumToArray(ServerConfig.PhotoProcessingLib).map((v) => {
|
||||
if (v.value.toLowerCase() === 'sharp') {
|
||||
v.value += ' ' + this.i18n('(recommended)');
|
||||
} else {
|
||||
v.value += ' ' + this.i18n('(deprecated, will be removed)');
|
||||
}
|
||||
return v;
|
||||
});
|
||||
readonly jobName = DefaultsJobs[DefaultsJobs['Photo Converting']];
|
||||
|
||||
constructor(_authService: AuthenticationService,
|
||||
@ -52,15 +43,24 @@ export class PhotoSettingsComponent extends SettingsComponent<{
|
||||
server: s.Server.Media.Photo
|
||||
}));
|
||||
const currentRes = _settingsService.Settings.value.Server.Media.Photo.Converting.resolution;
|
||||
if (this.resolutions.indexOf(currentRes) === -1) {
|
||||
this.resolutions.push(currentRes);
|
||||
if (this.resolutionTypes.indexOf(currentRes) === -1) {
|
||||
this.resolutionTypes.push(currentRes);
|
||||
}
|
||||
this.resolutions = this.resolutionTypes.map(e => ({key: e, value: e + 'px'}));
|
||||
}
|
||||
|
||||
|
||||
get Progress() {
|
||||
return this.jobsService.progress.value[JobDTO.getHashName(DefaultsJobs[DefaultsJobs['Photo Converting']])];
|
||||
}
|
||||
|
||||
libTypesMap = (v: { key: number, value: string }) => {
|
||||
if (v.value.toLowerCase() === 'sharp') {
|
||||
v.value += ' ' + this.i18n('(recommended)');
|
||||
} else {
|
||||
v.value += ' ' + this.i18n('(deprecated, will be removed)');
|
||||
}
|
||||
return v;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
@ -1,27 +1,29 @@
|
||||
<form #settingsForm="ngForm">
|
||||
<div class="card mb-4"
|
||||
[ngClass]="settings.enabled && !_settingsService.isSupported()?'panel-warning':''">
|
||||
[ngClass]="states.enabled.value && !_settingsService.isSupported()?'panel-warning':''">
|
||||
<h5 class="card-header">
|
||||
{{Name}}
|
||||
<div class="switch-wrapper">
|
||||
<bSwitch
|
||||
class="switch"
|
||||
name="enabled"
|
||||
[switch-on-color]="'success'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-disabled]="inProgress || (!settings.enabled && !_settingsService.isSupported())"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.enabled">
|
||||
switch-on-color="success"
|
||||
switch-inverse="true"
|
||||
switch-off-text="Disabled"
|
||||
switch-on-text="Enabled"
|
||||
i18n-switch-off-text
|
||||
i18n-switch-on-text
|
||||
[switch-disabled]="inProgress || !_settingsService.isSupported()"
|
||||
switch-handle-width="100"
|
||||
switch-label-width="20"
|
||||
[(ngModel)]="states.enabled.value">
|
||||
</bSwitch>
|
||||
</div>
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
|
||||
|
||||
<ng-container *ngIf="settings.enabled || _settingsService.isSupported()">
|
||||
<ng-container *ngIf="states.enabled.value || _settingsService.isSupported()">
|
||||
|
||||
<div class="alert alert-secondary" role="alert" i18n>
|
||||
This feature enables you to generate 'random photo' urls.
|
||||
@ -30,7 +32,7 @@
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
<div class="panel-info" *ngIf="(!settings.enabled && !_settingsService.isSupported())" i18n>
|
||||
<div class="panel-info" *ngIf="(!states.enabled.value && !_settingsService.isSupported())" i18n>
|
||||
Random Photo is not supported with these settings
|
||||
</div>
|
||||
<button class="btn btn-success float-right"
|
||||
|
@ -1,71 +1,53 @@
|
||||
<form #settingsForm="ngForm">
|
||||
<div class="card mb-4"
|
||||
[ngClass]="settings.enabled && !_settingsService.isSupported()?'panel-warning':''">
|
||||
[ngClass]="states.enabled.value && !_settingsService.isSupported()?'panel-warning':''">
|
||||
<h5 class="card-header">
|
||||
{{Name}}
|
||||
<div class="switch-wrapper">
|
||||
<bSwitch
|
||||
class="switch"
|
||||
name="enabled"
|
||||
[switch-on-color]="'success'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-disabled]="inProgress || (!settings.enabled && !_settingsService.isSupported())"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.enabled">
|
||||
switch-on-color="success"
|
||||
switch-inverse="true"
|
||||
switch-off-text="Disabled"
|
||||
switch-on-text="Enabled"
|
||||
i18n-switch-off-text
|
||||
i18n-switch-on-text
|
||||
[switch-disabled]="inProgress || !_settingsService.isSupported()"
|
||||
switch-handle-width="100"
|
||||
switch-label-width="20"
|
||||
[(ngModel)]="states.enabled.value">
|
||||
</bSwitch>
|
||||
</div>
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
|
||||
|
||||
<ng-container *ngIf="settings.enabled || _settingsService.isSupported()">
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="autocompleteEnabled" i18n>Autocomplete</label>
|
||||
<div class="col-md-10">
|
||||
<bSwitch
|
||||
id="autocompleteEnabled"
|
||||
class="switch"
|
||||
name="autocompleteEnabled"
|
||||
[switch-on-color]="'primary'"
|
||||
[switch-disabled]="!settings.enabled"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.AutoComplete.enabled">
|
||||
</bSwitch>
|
||||
<small class="form-text text-muted" i18n>Show hints while typing search query</small>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="states.enabled.value || _settingsService.isSupported()">
|
||||
|
||||
|
||||
|
||||
|
||||
<app-settings-entry
|
||||
name="Autocomplete"
|
||||
description="Show hints while typing search query."
|
||||
i18n-description i18n-name
|
||||
[ngModel]="states.AutoComplete.enabled"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
<app-settings-entry
|
||||
name="Instant search"
|
||||
description="Enables showing search results, while typing search query."
|
||||
i18n-description i18n-name
|
||||
[ngModel]="states.instantSearchEnabled"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="instantSearchEnabled" i18n>Instant search</label>
|
||||
<div class="col-md-10">
|
||||
<bSwitch
|
||||
id="instantSearchEnabled"
|
||||
class="switch"
|
||||
name="instantSearchEnabled"
|
||||
[switch-on-color]="'primary'"
|
||||
[switch-disabled]="!settings.enabled"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.instantSearchEnabled">
|
||||
</bSwitch>
|
||||
<small class="form-text text-muted" i18n>Enables showing search results, while typing search query</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
<div class="panel-info" *ngIf="(!settings.enabled && !_settingsService.isSupported())" i18n>
|
||||
<div class="panel-info" *ngIf="(!states.enabled.value && !_settingsService.isSupported())" i18n>
|
||||
Search is not supported with these settings
|
||||
</div>
|
||||
<button class="btn btn-success float-right"
|
||||
|
@ -1,50 +1,41 @@
|
||||
<form #settingsForm="ngForm">
|
||||
<div class="card mb-4"
|
||||
[ngClass]="settings.enabled && !_settingsService.isSupported()?'panel-warning':''">
|
||||
[ngClass]="states.enabled.value && !_settingsService.isSupported()?'panel-warning':''">
|
||||
<h5 class="card-header">
|
||||
{{Name}}
|
||||
<div class="switch-wrapper">
|
||||
<bSwitch
|
||||
class="switch"
|
||||
name="enabled"
|
||||
[switch-on-color]="'success'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-disabled]="inProgress || (!settings.enabled && !_settingsService.isSupported())"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.enabled">
|
||||
switch-on-color="success"
|
||||
switch-inverse="true"
|
||||
switch-off-text="Disabled"
|
||||
switch-on-text="Enabled"
|
||||
i18n-switch-off-text
|
||||
i18n-switch-on-text
|
||||
[switch-disabled]="inProgress || !_settingsService.isSupported()"
|
||||
switch-handle-width="100"
|
||||
switch-label-width="20"
|
||||
[(ngModel)]="states.enabled.value">
|
||||
</bSwitch>
|
||||
</div>
|
||||
</h5>
|
||||
<div class="card-body">
|
||||
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
|
||||
|
||||
<ng-container *ngIf="settings.enabled || _settingsService.isSupported()">
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="passwordProtected" i18n>Password protected</label>
|
||||
<div class="col-md-10">
|
||||
<bSwitch
|
||||
id="passwordProtected"
|
||||
class="switch"
|
||||
name="passwordProtected"
|
||||
[switch-on-color]="'primary'"
|
||||
[switch-disabled]="!settings.enabled"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.passwordProtected">
|
||||
</bSwitch>
|
||||
<small class="form-text text-muted" i18n>Enables password protected sharing links</small>
|
||||
<ng-container *ngIf="states.enabled.value || _settingsService.isSupported()">
|
||||
|
||||
<app-settings-entry
|
||||
name="Password protected"
|
||||
description="Enables password protected sharing links."
|
||||
i18n-description i18n-name
|
||||
[ngModel]="states.passwordProtected"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
<div class="panel-info" *ngIf="(!settings.enabled && !_settingsService.isSupported())" i18n>
|
||||
<div class="panel-info" *ngIf="(!states.enabled.value && !_settingsService.isSupported())" i18n>
|
||||
Sharing is not supported with these settings
|
||||
</div>
|
||||
<button class="btn btn-success float-right"
|
||||
|
@ -7,48 +7,33 @@
|
||||
<div [hidden]="!error" class="alert alert-danger" role="alert"><strong>Error: </strong>{{error}}</div>
|
||||
|
||||
|
||||
<div class="form-group row" [hidden]="simplifiedMode">
|
||||
<label class="col-md-2 control-label" for="quality" i18n>Thumbnail Quality</label>
|
||||
<div class="col-md-10">
|
||||
<bSwitch
|
||||
id="quality"
|
||||
class="switch"
|
||||
name="enabled"
|
||||
[switch-on-color]="'primary'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Low"
|
||||
[switch-on-text]="text.High"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.server.qualityPriority">
|
||||
</bSwitch>
|
||||
<small class="form-text text-muted" i18n>High quality may be slow. Especially with Jimp.</small>
|
||||
<app-settings-entry
|
||||
name="Thumbnail Quality"
|
||||
description="High quality may be slow. Especially with Jimp."
|
||||
i18n-description i18n-name
|
||||
[ngModel]="states.server.qualityPriority"
|
||||
[simplifiedMode]="simplifiedMode"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row" [hidden]="simplifiedMode">
|
||||
<label class="col-md-2 control-label" for="icon" i18n>Icon size</label>
|
||||
<div class="col-md-10">
|
||||
<input type="number" class="form-control" placeholder="30"
|
||||
id="icon"
|
||||
[(ngModel)]="settings.client.iconSize"
|
||||
min="1"
|
||||
max="100"
|
||||
step="1"
|
||||
name="icon" required>
|
||||
<small class="form-text text-muted" i18n>Icon size (used on maps)</small>
|
||||
<app-settings-entry
|
||||
name="Icon size"
|
||||
description="Icon size (used on maps)."
|
||||
i18n-description i18n-name
|
||||
[ngModel]="states.client.iconSize"
|
||||
[simplifiedMode]="simplifiedMode"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
</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>
|
||||
<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/>
|
||||
@ -60,9 +45,10 @@
|
||||
pixels.
|
||||
</ng-container>
|
||||
</small>
|
||||
</app-settings-entry>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn btn-success float-right"
|
||||
[disabled]="!settingsForm.form.valid || !changed || inProgress"
|
||||
|
@ -37,20 +37,9 @@ export class ThumbnailSettingsComponent
|
||||
}
|
||||
|
||||
get Config(): any {
|
||||
return {sizes: this.original.client.thumbnailSizes[0]};
|
||||
return {sizes: this.states.client.thumbnailSizes.original[0]};
|
||||
}
|
||||
|
||||
get ThumbnailSizes(): string {
|
||||
return this.settings.client.thumbnailSizes.join('; ');
|
||||
}
|
||||
|
||||
set ThumbnailSizes(value: string) {
|
||||
value = value.replace(new RegExp(',', 'g'), ';');
|
||||
value = value.replace(new RegExp(' ', 'g'), ';');
|
||||
this.settings.client.thumbnailSizes = value.split(';')
|
||||
.map(s => parseInt(s, 10))
|
||||
.filter(i => !isNaN(i) && i > 0);
|
||||
}
|
||||
|
||||
get Progress() {
|
||||
return this.jobsService.progress.value[JobDTO.getHashName(this.jobName, this.Config)];
|
||||
|
@ -5,12 +5,12 @@
|
||||
<bSwitch
|
||||
class="switch"
|
||||
name="enabled"
|
||||
[switch-on-color]="'success'"
|
||||
[switch-inverse]="true"
|
||||
switch-on-color="success"
|
||||
switch-inverse="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
switch-handle-width="100"
|
||||
switch-label-width="20"
|
||||
[switch-disabled]="inProgress"
|
||||
[(ngModel)]="enabled"
|
||||
(changeState)="switched($event)">
|
||||
|
@ -7,14 +7,16 @@
|
||||
<bSwitch
|
||||
class="switch"
|
||||
name="enabled"
|
||||
[switch-on-color]="'success'"
|
||||
[switch-inverse]="true"
|
||||
[switch-off-text]="text.Disabled"
|
||||
[switch-on-text]="text.Enabled"
|
||||
switch-on-color="success"
|
||||
switch-inverse="true"
|
||||
switch-off-text="Disabled"
|
||||
switch-on-text="Enabled"
|
||||
i18n-switch-off-text
|
||||
i18n-switch-on-text
|
||||
[switch-disabled]="inProgress"
|
||||
[switch-handle-width]="100"
|
||||
[switch-label-width]="20"
|
||||
[(ngModel)]="settings.client.enabled">
|
||||
switch-handle-width="100"
|
||||
switch-label-width="20"
|
||||
[(ngModel)]="states.client.enabled.value">
|
||||
</bSwitch>
|
||||
</div>
|
||||
</h5>
|
||||
@ -42,58 +44,50 @@
|
||||
</ng-container>
|
||||
</div>
|
||||
|
||||
<div class="form-group row" [hidden]="simplifiedMode">
|
||||
<label class="col-md-2 control-label" for="format" i18n>Format</label>
|
||||
<div class="col-md-10">
|
||||
<select id="format" class="form-control" [(ngModel)]="settings.server.transcoding.format"
|
||||
<app-settings-entry
|
||||
name="Format"
|
||||
i18n-name
|
||||
[ngModel]="states.server.transcoding.format"
|
||||
[simplifiedMode]="simplifiedMode"
|
||||
(ngModelChange)="formatChanged($event)"
|
||||
name="format" required>
|
||||
<option *ngFor="let format of formats" [ngValue]="format">{{format}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
[options]="formats"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
<div class="form-group row" [hidden]="simplifiedMode">
|
||||
<label class="col-md-2 control-label" for="codec" i18n>Codec</label>
|
||||
<div class="col-md-10">
|
||||
<select id="codec" class="form-control" [(ngModel)]="settings.server.transcoding.codec"
|
||||
name="codec" required>
|
||||
<option *ngFor="let codec of codecs[settings.server.transcoding.format]" [ngValue]="codec">{{codec}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<app-settings-entry
|
||||
name="Codec"
|
||||
i18n-name
|
||||
[ngModel]="states.server.transcoding.codec"
|
||||
[simplifiedMode]="simplifiedMode"
|
||||
[options]="codecs[states.server.transcoding.format.value]"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
<div class="form-group row">
|
||||
<label class="col-md-2 control-label" for="resolution" i18n>Resolution</label>
|
||||
<div class="col-md-10">
|
||||
<select id="resolution" class="form-control" [(ngModel)]="settings.server.transcoding.resolution"
|
||||
<app-settings-entry
|
||||
name="Resolution"
|
||||
description="The height of the output video will be scaled down to this, while keeping the aspect ratio."
|
||||
i18n-name i18n-description
|
||||
[ngModel]="states.server.transcoding.resolution"
|
||||
(ngModelChange)="updateBitRate()"
|
||||
name="resolution" required>
|
||||
<option *ngFor="let resolution of resolutions" [ngValue]="resolution">{{resolution}}p
|
||||
</option>
|
||||
</select>
|
||||
<small class="form-text text-muted" i18n>The height of the output video will be scaled down to this, while
|
||||
keeping the aspect ratio.</small>
|
||||
</div>
|
||||
</div>
|
||||
[options]="resolutions"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
<div class="form-group row" [hidden]="simplifiedMode">
|
||||
<label class="col-md-2 control-label" for="fps" i18n>FPS</label>
|
||||
<div class="col-md-10">
|
||||
<select id="fps" class="form-control" [(ngModel)]="settings.server.transcoding.fps"
|
||||
|
||||
<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()"
|
||||
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>
|
||||
[options]="fps"
|
||||
[simplifiedMode]="simplifiedMode"
|
||||
required="true">
|
||||
</app-settings-entry>
|
||||
|
||||
<div class="form-group row">
|
||||
|
||||
<div class="form-group row"
|
||||
[class.changed-settings]="states.server.transcoding.bitRate.value !== states.server.transcoding.bitRate.default">
|
||||
<label class="col-md-2 control-label" for="bitRate" i18n>Bit rate</label>
|
||||
<div class="col-md-10">
|
||||
<div class="input-group">
|
||||
@ -115,9 +109,6 @@
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<button class="btn btn-success float-right"
|
||||
[disabled]="!settingsForm.form.valid || !changed || inProgress"
|
||||
(click)="save()" i18n>Save
|
||||
|
@ -21,10 +21,16 @@ import {ClientConfig} from '../../../../../common/config/public/ClientConfig';
|
||||
})
|
||||
export class VideoSettingsComponent extends SettingsComponent<{ server: ServerConfig.VideoConfig, client: ClientConfig.VideoConfig }> {
|
||||
|
||||
resolutions: ServerConfig.resolutionType[] = [360, 480, 720, 1080, 1440, 2160, 4320];
|
||||
codecs: { [key: string]: ServerConfig.codecType[] } = {webm: ['libvpx', 'libvpx-vp9'], mp4: ['libx264', 'libx265']};
|
||||
formats: ServerConfig.formatType[] = ['mp4', 'webm'];
|
||||
fps = [24, 25, 30, 48, 50, 60];
|
||||
readonly resolutionTypes: ServerConfig.resolutionType[] = [360, 480, 720, 1080, 1440, 2160, 4320];
|
||||
|
||||
resolutions: { key: number, value: string }[] = [];
|
||||
codecs: { [key: string]: { key: ServerConfig.codecType, value: ServerConfig.codecType }[] } = {
|
||||
webm: ['libvpx', 'libvpx-vp9'].map((e: ServerConfig.codecType) => ({key: e, value: e})),
|
||||
mp4: ['libx264', 'libx265'].map((e: ServerConfig.codecType) => ({key: e, value: e}))
|
||||
};
|
||||
formats: { key: ServerConfig.formatType, value: ServerConfig.formatType }[] = ['mp4', 'webm']
|
||||
.map((e: ServerConfig.formatType) => ({key: e, value: e}));
|
||||
fps = [24, 25, 30, 48, 50, 60].map(e => ({key: e, value: e}));
|
||||
|
||||
JobProgressStates = JobProgressStates;
|
||||
readonly jobName = DefaultsJobs[DefaultsJobs['Video Converting']];
|
||||
@ -41,9 +47,10 @@ export class VideoSettingsComponent extends SettingsComponent<{ server: ServerCo
|
||||
}));
|
||||
|
||||
const currentRes = _settingsService.Settings.value.Server.Media.Video.transcoding.resolution;
|
||||
if (this.resolutions.indexOf(currentRes) === -1) {
|
||||
this.resolutions.push(currentRes);
|
||||
if (this.resolutionTypes.indexOf(currentRes) === -1) {
|
||||
this.resolutionTypes.push(currentRes);
|
||||
}
|
||||
this.resolutions = this.resolutionTypes.map(e => ({key: e, value: e + 'px'}));
|
||||
}
|
||||
|
||||
|
||||
@ -52,11 +59,11 @@ export class VideoSettingsComponent extends SettingsComponent<{ server: ServerCo
|
||||
}
|
||||
|
||||
get bitRate(): number {
|
||||
return this.settings.server.transcoding.bitRate / 1024 / 1024;
|
||||
return this.states.server.transcoding.bitRate.value / 1024 / 1024;
|
||||
}
|
||||
|
||||
set bitRate(value: number) {
|
||||
this.settings.server.transcoding.bitRate = Math.round(value * 1024 * 1024);
|
||||
this.states.server.transcoding.bitRate.value = Math.round(value * 1024 * 1024);
|
||||
}
|
||||
|
||||
getRecommendedBitRate(resolution: number, fps: number) {
|
||||
@ -83,12 +90,12 @@ export class VideoSettingsComponent extends SettingsComponent<{ server: ServerCo
|
||||
}
|
||||
|
||||
updateBitRate() {
|
||||
this.settings.server.transcoding.bitRate = this.getRecommendedBitRate(this.settings.server.transcoding.resolution,
|
||||
this.settings.server.transcoding.fps);
|
||||
this.states.server.transcoding.bitRate.value = this.getRecommendedBitRate(this.states.server.transcoding.resolution.value,
|
||||
this.states.server.transcoding.fps.value);
|
||||
}
|
||||
|
||||
formatChanged(format: ServerConfig.formatType) {
|
||||
this.settings.server.transcoding.codec = this.codecs[format][0];
|
||||
this.states.server.transcoding.codec.value = this.codecs[format][0].key;
|
||||
}
|
||||
|
||||
|
||||
|
@ -12,6 +12,10 @@
|
||||
margin-left: -4px;
|
||||
}
|
||||
|
||||
.changed-settings .bootstrap-switch{
|
||||
border-color: #007bff;
|
||||
border-width: 1px;
|
||||
}
|
||||
|
||||
#toast-container > div {
|
||||
opacity:1;
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -42,7 +42,7 @@ describe('SettingsRouter', () => {
|
||||
result.res.should.have.status(200);
|
||||
result.body.should.be.a('object');
|
||||
should.equal(result.body.error, null);
|
||||
result.body.result.should.deep.equal(JSON.parse(JSON.stringify(originalSettings.toJSON({attachDefaults: true}))));
|
||||
result.body.result.should.deep.equal(JSON.parse(JSON.stringify(originalSettings.toJSON({attachState: true, attachVolatile: true}))));
|
||||
|
||||
});
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user