You've already forked pigallery2
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:
@@ -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) {
|
||||
|
@@ -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) {
|
||||
|
@@ -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),
|
||||
|
@@ -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,
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -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">
|
||||
|
@@ -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,
|
||||
|
@@ -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})));
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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]}]"
|
||||
|
@@ -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) {
|
||||
}
|
||||
|
||||
|
@@ -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)) {
|
||||
|
@@ -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,
|
||||
|
@@ -0,0 +1,3 @@
|
||||
.panel-info {
|
||||
text-align: center;
|
||||
}
|
72
frontend/app/ui/settings/faces/faces.settings.component.html
Normal file
72
frontend/app/ui/settings/faces/faces.settings.component.html
Normal 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>
|
40
frontend/app/ui/settings/faces/faces.settings.component.ts
Normal file
40
frontend/app/ui/settings/faces/faces.settings.component.ts
Normal 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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
23
frontend/app/ui/settings/faces/faces.settings.service.ts
Normal file
23
frontend/app/ui/settings/faces/faces.settings.service.ts
Normal 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});
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user