1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-09-16 09:16:27 +02:00

adding config ui for faces

This commit is contained in:
Patrik J. Braun
2019-07-21 16:39:52 +02:00
parent 384d366cd3
commit 685c406e91
18 changed files with 225 additions and 12 deletions

View File

@@ -253,6 +253,30 @@ export class AdminMWs {
return next(new ErrorDTO(ErrorCodes.SETTINGS_ERROR, 'Settings error: ' + JSON.stringify(err, null, ' '), err));
}
}
public static async updateFacesSettings(req: Request, res: Response, next: NextFunction) {
if ((typeof req.body === 'undefined') || (typeof req.body.settings === 'undefined')) {
return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'settings is needed'));
}
try {
// only updating explicitly set config (not saving config set by the diagnostics)
const original = Config.original();
await ConfigDiagnostics.testFacesConfig(<ClientConfig.FacesConfig>req.body.settings, original);
Config.Client.Faces = <ClientConfig.FacesConfig>req.body.settings;
original.Client.Faces = <ClientConfig.FacesConfig>req.body.settings;
original.save();
await ConfigDiagnostics.runDiagnostics();
Logger.info(LOG_TAG, 'new config:');
Logger.info(LOG_TAG, JSON.stringify(Config, null, '\t'));
return next();
} catch (err) {
if (err instanceof Error) {
return next(new ErrorDTO(ErrorCodes.SETTINGS_ERROR, 'Settings error: ' + err.toString(), err));
}
return next(new ErrorDTO(ErrorCodes.SETTINGS_ERROR, 'Settings error: ' + JSON.stringify(err, null, ' '), err));
}
}
public static async updateAuthenticationSettings(req: Request, res: Response, next: NextFunction) {

View File

@@ -127,6 +127,13 @@ export class ConfigDiagnostics {
}
static async testFacesConfig(faces: ClientConfig.FacesConfig, config: IPrivateConfig) {
if (faces.enabled === true &&
config.Server.database.type === DatabaseType.memory) {
throw new Error('Memory Database do not support faces');
}
}
static async testSearchConfig(search: ClientConfig.SearchConfig, config: IPrivateConfig) {
if (search.enabled === true &&
config.Server.database.type === DatabaseType.memory) {
@@ -260,6 +267,16 @@ export class ConfigDiagnostics {
Config.Client.Search.enabled = false;
}
try {
await ConfigDiagnostics.testFacesConfig(Config.Client.Faces, Config);
} catch (ex) {
const err: Error = ex;
NotificationManager.warning('Faces are not supported with these settings. Disabling temporally. ' +
'Please adjust the config properly.', err.toString());
Logger.warn(LOG_TAG, 'Faces are not supported with these settings, switching off..', err.toString());
Config.Client.Faces.enabled = false;
}
try {
await ConfigDiagnostics.testSharingConfig(Config.Client.Sharing, Config);
} catch (ex) {

View File

@@ -109,6 +109,12 @@ export class AdminRouter {
AdminMWs.updateSearchSettings,
RenderingMWs.renderOK
);
app.put('/api/settings/faces',
AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin),
AdminMWs.updateFacesSettings,
RenderingMWs.renderOK
);
app.put('/api/settings/share',
AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin),

View File

@@ -80,6 +80,7 @@ import {FaceComponent} from './ui/faces/face/face.component';
import {VersionService} from './model/version.service';
import { DirectoriesComponent } from './ui/gallery/directories/directories.component';
import {ControlsLightboxComponent} from './ui/gallery/lightbox/controls/controls.lightbox.gallery.component';
import {FacesSettingsComponent} from './ui/settings/faces/faces.settings.component';
@Injectable()
@@ -178,6 +179,7 @@ export function translationsFactory(locale: string) {
ShareSettingsComponent,
RandomPhotoSettingsComponent,
BasicSettingsComponent,
FacesSettingsComponent,
OtherSettingsComponent,
IndexingSettingsComponent,
DuplicateComponent,

View File

@@ -62,6 +62,8 @@
[simplifiedMode]="simplifiedMode"></app-settings-other>
<app-settings-random-photo #random [hidden]="!random.hasAvailableSettings"
[simplifiedMode]="simplifiedMode"></app-settings-random-photo>
<app-settings-faces #random [hidden]="!random.hasAvailableSettings"
[simplifiedMode]="simplifiedMode"></app-settings-faces>
<app-settings-indexing #indexing [hidden]="!indexing.hasAvailableSettings"
[simplifiedMode]="simplifiedMode"></app-settings-indexing>
</div>

View File

@@ -2,3 +2,12 @@ app-face {
margin: 2px;
display: inline-block;
}
.no-face-msg{
height: 100vh;
text-align: center;
}
.no-face-msg h2{
color: #6c757d;
}

View File

@@ -4,9 +4,17 @@
<app-face *ngFor="let person of favourites | async"
[person]="person"
[size]="size"></app-face>
<hr/>
<hr *ngIf="(nonFavourites | async).length > 0"/>
<app-face *ngFor="let person of nonFavourites | async"
[person]="person"
[size]="size"></app-face>
<div class="d-flex no-face-msg"
*ngIf="(nonFavourites | async).length == 0 && (favourites | async).length == 0">
<div class="flex-fill">
<h2>:( <ng-container i18n>No faces to show.</ng-container>
</h2>
</div>
</div>
</div>
</app-frame>

View File

@@ -15,10 +15,10 @@
<li class="nav-item" [routerLinkActive]="['active']">
<a class="nav-link"
[routerLink]="['/gallery']"
[queryParams]="queryService.getParams()">Gallery</a>
[queryParams]="queryService.getParams()" i18n>Gallery</a>
</li>
<li class="nav-item" [routerLinkActive]="['active']">
<a class="nav-link" [routerLink]="['/faces']">Faces</a>
<li class="nav-item" [routerLinkActive]="['active']" *ngIf="facesEnabled">
<a class="nav-link" [routerLink]="['/faces']" i18n>Faces</a>
</li>
</ul>
<ul class="navbar-nav navbar-right ml-auto">

View File

@@ -20,6 +20,7 @@ export class FrameComponent {
authenticationRequired = false;
public title: string;
collapsed = true;
facesEnabled = Config.Client.Faces.enabled;
constructor(private _authService: AuthenticationService,
public notificationService: NotificationService,

View File

@@ -57,10 +57,13 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy {
const metadata = this.gridMedia.media.metadata as PhotoMetadata;
if ((metadata.keywords && metadata.keywords.length > 0) ||
(metadata.faces && metadata.faces.length > 0)) {
const names: string[] = (metadata.faces || []).map(f => f.name);
this.keywords = names.filter((name, index) => names.indexOf(name) === index)
.map(n => ({value: n, type: SearchTypes.person}))
.concat((metadata.keywords || []).map(k => ({value: k, type: SearchTypes.keyword})));
this.keywords = [];
if (Config.Client.Faces.enabled) {
const names: string[] = (metadata.faces || []).map(f => f.name);
this.keywords = names.filter((name, index) => names.indexOf(name) === index)
.map(n => ({value: n, type: SearchTypes.person}));
}
this.keywords = this.keywords.concat((metadata.keywords || []).map(k => ({value: k, type: SearchTypes.keyword})));
}
}

View File

@@ -55,7 +55,7 @@
[style.left.px]="photoFrameDim.width/2"
[style.width.px]="faceContainerDim.width"
[style.height.px]="faceContainerDim.height"
*ngIf="activePhoto && zoom == 1">
*ngIf="facesEnabled && activePhoto && zoom == 1">
<a
class="face"
[routerLink]="['/search', face.name, {type: SearchTypes[SearchTypes.person]}]"

View File

@@ -7,6 +7,7 @@ import {filter} from 'rxjs/operators';
import {PhotoDTO} from '../../../../../../common/entities/PhotoDTO';
import {GalleryLightboxMediaComponent} from '../media/media.lightbox.gallery.component';
import {SearchTypes} from '../../../../../../common/entities/AutoCompleteItem';
import {Config} from '../../../../../../common/config/public/Config';
export enum PlayBackStates {
Paused = 1,
@@ -36,6 +37,7 @@ export class ControlsLightboxComponent implements OnDestroy, OnInit, OnChanges {
@Input() mediaElement: GalleryLightboxMediaComponent;
@Input() photoFrameDim = {width: 1, height: 1, aspect: 1};
public readonly facesEnabled = Config.Client.Faces.enabled;
public zoom = 1;
public playBackState: PlayBackStates = PlayBackStates.Paused;
@@ -44,6 +46,7 @@ export class ControlsLightboxComponent implements OnDestroy, OnInit, OnChanges {
public controllersAlwaysOn = false;
public controllersVisible = true;
public drag = {x: 0, y: 0};
public SearchTypes = SearchTypes;
private visibilityTimer: number = null;
private timer: Observable<number>;
private timerSub: Subscription;
@@ -51,8 +54,6 @@ export class ControlsLightboxComponent implements OnDestroy, OnInit, OnChanges {
private prevZoom = 1;
private faceContainerDim = {width: 0, height: 0};
public SearchTypes = SearchTypes;
constructor(public fullScreenService: FullScreenService) {
}

View File

@@ -54,6 +54,8 @@ export class GalleryMapLightboxComponent implements OnChanges, AfterViewInit {
}
ngAfterViewInit() {
// TODO: remove it once yaga/leaflet-ng2 is fixes.
// See issue: https://github.com/yagajs/leaflet-ng2/issues/440
let i = 0;
this.yagaMap.eachLayer(l => {
if (i >= 3 || (this.paths.length === 0 && i >= 2)) {

View File

@@ -38,7 +38,7 @@ export abstract class SettingsComponent<T extends { [key: string]: any }, S exte
private readonly _settingsSubscription: Subscription = null;
protected constructor(private name: string,
private _authService: AuthenticationService,
protected _authService: AuthenticationService,
private _navigation: NavigationService,
public _settingsService: S,
protected notification: NotificationService,

View File

@@ -0,0 +1,3 @@
.panel-info {
text-align: center;
}

View File

@@ -0,0 +1,72 @@
<form #settingsForm="ngForm">
<div class="card mb-4"
[ngClass]="settings.enabled && !_settingsService.isSupported()?'panel-warning':''">
<h5 class="card-header">
<ng-container i18n>Faces settings</ng-container>
<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">
</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>
<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>
</ng-container>
<div class="panel-info" *ngIf="(!settings.enabled && !_settingsService.isSupported())" i18n>
Faces are not supported with these settings.
</div>
<button class="btn btn-success float-right"
[disabled]="!settingsForm.form.valid || !changed || inProgress"
(click)="save()" i18n>Save
</button>
<button class="btn btn-secondary float-right"
(click)="reset()" i18n>Reset
</button>
</div>
</div>
</form>

View File

@@ -0,0 +1,40 @@
import {Component} from '@angular/core';
import {SettingsComponent} from '../_abstract/abstract.settings.component';
import {AuthenticationService} from '../../../model/network/authentication.service';
import {NavigationService} from '../../../model/navigation.service';
import {NotificationService} from '../../../model/notification.service';
import {ClientConfig} from '../../../../../common/config/public/ConfigClass';
import {FacesSettingsService} from './faces.settings.service';
import {I18n} from '@ngx-translate/i18n-polyfill';
import {Utils} from '../../../../../common/Utils';
import {UserRoles} from '../../../../../common/entities/UserDTO';
@Component({
selector: 'app-settings-faces',
templateUrl: './faces.settings.component.html',
styleUrls: ['./faces.settings.component.css',
'./../_abstract/abstract.settings.component.css'],
providers: [FacesSettingsService],
})
export class FacesSettingsComponent extends SettingsComponent<ClientConfig.FacesConfig> {
public userRoles: Array<any> = [];
constructor(_authService: AuthenticationService,
_navigation: NavigationService,
_settingsService: FacesSettingsService,
notification: NotificationService,
i18n: I18n) {
super(i18n('Faces'), _authService, _navigation, _settingsService, notification, i18n, s => s.Client.Faces);
this.userRoles = Utils
.enumToArray(UserRoles)
.filter(r => r.key !== UserRoles.LimitedGuest)
.filter(r => r.key <= this._authService.user.value.role)
.sort((a, b) => a.key - b.key);
}
}

View File

@@ -0,0 +1,23 @@
import {Injectable} from '@angular/core';
import {NetworkService} from '../../../model/network/network.service';
import {DatabaseType} from '../../../../../common/config/private/IPrivateConfig';
import {ClientConfig} from '../../../../../common/config/public/ConfigClass';
import {SettingsService} from '../settings.service';
import {AbstractSettingsService} from '../_abstract/abstract.settings.service';
@Injectable()
export class FacesSettingsService extends AbstractSettingsService<ClientConfig.FacesConfig> {
constructor(private _networkService: NetworkService,
_settingsService: SettingsService) {
super(_settingsService);
}
public isSupported(): boolean {
return this._settingsService.settings.value.Server.database.type !== DatabaseType.memory;
}
public updateSettings(settings: ClientConfig.FacesConfig): Promise<void> {
return this._networkService.putJson('/settings/faces', {settings: settings});
}
}