diff --git a/backend/middlewares/PersonMWs.ts b/backend/middlewares/PersonMWs.ts index 0bbeb9c..61a7031 100644 --- a/backend/middlewares/PersonMWs.ts +++ b/backend/middlewares/PersonMWs.ts @@ -10,6 +10,22 @@ const LOG_TAG = '[PersonMWs]'; export class PersonMWs { + public static async updatePerson(req: Request, res: Response, next: NextFunction) { + if (!req.params.name) { + return next(); + } + + try { + req.resultPipe = await ObjectManagers.getInstance() + .PersonManager.updatePerson(req.params.name as string, + req.body as PersonDTO); + return next(); + + } catch (err) { + return next(new ErrorDTO(ErrorCodes.PERSON_ERROR, 'Error during updating a person', err)); + } + } + public static async listPersons(req: Request, res: Response, next: NextFunction) { try { req.resultPipe = await ObjectManagers.getInstance() @@ -18,7 +34,7 @@ export class PersonMWs { return next(); } catch (err) { - return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'Error during listing the directory', err)); + return next(new ErrorDTO(ErrorCodes.PERSON_ERROR, 'Error during listing persons', err)); } } @@ -37,7 +53,7 @@ export class PersonMWs { return next(); } catch (err) { - return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'Error during listing the directory', err)); + return next(new ErrorDTO(ErrorCodes.PERSON_ERROR, 'Error during adding sample photo for all persons', err)); } } @@ -55,7 +71,7 @@ export class PersonMWs { return next(); } catch (err) { - return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'Error during listing the directory', err)); + return next(new ErrorDTO(ErrorCodes.PERSON_ERROR, 'Error during removing sample photo from all persons', err)); } } @@ -76,7 +92,7 @@ export class PersonMWs { return next(); } catch (err) { - return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'Error during listing the directory', err)); + return next(new ErrorDTO(ErrorCodes.PERSON_ERROR, 'Error during getting sample photo for a person', err)); } } diff --git a/backend/model/interfaces/IPersonManager.ts b/backend/model/interfaces/IPersonManager.ts index f19296b..12458dc 100644 --- a/backend/model/interfaces/IPersonManager.ts +++ b/backend/model/interfaces/IPersonManager.ts @@ -1,6 +1,7 @@ import {PersonEntry} from '../sql/enitites/PersonEntry'; import {MediaDTO} from '../../../common/entities/MediaDTO'; import {PhotoDTO} from '../../../common/entities/PhotoDTO'; +import {PersonDTO} from '../../../common/entities/PersonDTO'; export interface IPersonManager { getAll(): Promise; @@ -14,4 +15,6 @@ export interface IPersonManager { keywordsToPerson(media: MediaDTO[]): Promise; updateCounts(): Promise; + + updatePerson(name: string, partialPerson: PersonDTO): Promise; } diff --git a/backend/model/memory/PersonManager.ts b/backend/model/memory/PersonManager.ts index d1e79bc..a5d618d 100644 --- a/backend/model/memory/PersonManager.ts +++ b/backend/model/memory/PersonManager.ts @@ -1,10 +1,11 @@ import {IPersonManager} from '../interfaces/IPersonManager'; import {MediaDTO} from '../../../common/entities/MediaDTO'; -import {PersonEntry} from '../sql/enitites/PersonEntry'; import {PhotoDTO} from '../../../common/entities/PhotoDTO'; +import {PersonDTO} from '../../../common/entities/PersonDTO'; export class PersonManager implements IPersonManager { - getAll(): Promise { + + getAll(): Promise { throw new Error('Method not implemented.'); } @@ -27,4 +28,8 @@ export class PersonManager implements IPersonManager { updateCounts(): Promise { throw new Error('not supported by memory DB'); } + + updatePerson(name: string, partialPerson: PersonDTO): Promise { + throw new Error('not supported by memory DB'); + } } diff --git a/backend/model/sql/PersonManager.ts b/backend/model/sql/PersonManager.ts index e9d2059..2dff926 100644 --- a/backend/model/sql/PersonManager.ts +++ b/backend/model/sql/PersonManager.ts @@ -5,12 +5,34 @@ import {MediaDTO} from '../../../common/entities/MediaDTO'; import {PhotoDTO} from '../../../common/entities/PhotoDTO'; import {MediaEntity} from './enitites/MediaEntity'; import {FaceRegionEntry} from './enitites/FaceRegionEntry'; +import {PersonDTO} from '../../../common/entities/PersonDTO'; const LOG_TAG = '[PersonManager]'; export class PersonManager implements IPersonManager { persons: PersonEntry[] = []; + async updatePerson(name: string, partialPerson: PersonDTO): Promise { + const connection = await SQLConnection.getConnection(); + const repository = connection.getRepository(PersonEntry); + const person = await repository.createQueryBuilder('person') + .limit(1) + .where('person.name LIKE :name COLLATE utf8_general_ci', {name: name}).getOne(); + + + if (typeof partialPerson.name !== 'undefined') { + person.name = partialPerson.name; + } + if (typeof partialPerson.isFavourite !== 'undefined') { + person.isFavourite = partialPerson.isFavourite; + } + await repository.save(person); + + await this.loadAll(); + + return person; + } + async getSamplePhoto(name: string): Promise { const connection = await SQLConnection.getConnection(); const rawAndEntities = await connection.getRepository(MediaEntity).createQueryBuilder('media') @@ -18,7 +40,7 @@ export class PersonManager implements IPersonManager { .leftJoinAndSelect('media.directory', 'directory') .leftJoinAndSelect('media.metadata.faces', 'faces') .leftJoinAndSelect('faces.person', 'person') - .where('person.name LIKE :name COLLATE utf8_general_ci', {name: '%' + name + '%'}).getRawAndEntities(); + .where('person.name LIKE :name COLLATE utf8_general_ci', {name: name}).getRawAndEntities(); if (rawAndEntities.entities.length === 0) { return null; diff --git a/backend/model/sql/enitites/PersonEntry.ts b/backend/model/sql/enitites/PersonEntry.ts index 7cd0f4e..bbd6d23 100644 --- a/backend/model/sql/enitites/PersonEntry.ts +++ b/backend/model/sql/enitites/PersonEntry.ts @@ -16,6 +16,9 @@ export class PersonEntry implements PersonDTO { @Column('int', {unsigned: true, default: 0}) count: number; + @Column({default: false}) + isFavourite: boolean; + @OneToMany(type => FaceRegionEntry, faceRegion => faceRegion.person) public faces: FaceRegionEntry[]; diff --git a/backend/routes/PersonRouter.ts b/backend/routes/PersonRouter.ts index 5df5026..d95b451 100644 --- a/backend/routes/PersonRouter.ts +++ b/backend/routes/PersonRouter.ts @@ -5,14 +5,30 @@ import {UserRoles} from '../../common/entities/UserDTO'; import {PersonMWs} from '../middlewares/PersonMWs'; import {ThumbnailGeneratorMWs} from '../middlewares/thumbnail/ThumbnailGeneratorMWs'; import {VersionMWs} from '../middlewares/VersionMWs'; +import {Config} from '../../common/config/private/Config'; export class PersonRouter { public static route(app: Express) { + this.updatePerson(app); this.addPersons(app); this.getPersonThumbnail(app); } + + private static updatePerson(app: Express) { + app.post(['/api/person/:name'], + // common part + AuthenticationMWs.authenticate, + AuthenticationMWs.authorise(Config.Client.Faces.writeAccessMinRole), + VersionMWs.injectGalleryVersion, + + // specific part + PersonMWs.updatePerson, + RenderingMWs.renderResult + ); + } + private static addPersons(app: Express) { app.get(['/api/person'], // common part diff --git a/common/DataStructureVersion.ts b/common/DataStructureVersion.ts index 573de74..67d21a2 100644 --- a/common/DataStructureVersion.ts +++ b/common/DataStructureVersion.ts @@ -1 +1 @@ -export const DataStructureVersion = 11; +export const DataStructureVersion = 12; diff --git a/common/config/public/ConfigClass.ts b/common/config/public/ConfigClass.ts index 6e9df03..77da586 100644 --- a/common/config/public/ConfigClass.ts +++ b/common/config/public/ConfigClass.ts @@ -69,6 +69,7 @@ export module ClientConfig { export interface FacesConfig { enabled: boolean; keywordsToPersons: boolean; + writeAccessMinRole: UserRoles; } export interface Config { @@ -147,7 +148,8 @@ export class PublicConfigClass { }, Faces: { enabled: true, - keywordsToPersons: true + keywordsToPersons: true, + writeAccessMinRole: UserRoles.Admin }, authenticationRequired: true, unAuthenticatedUserRole: UserRoles.Admin, diff --git a/common/entities/Error.ts b/common/entities/Error.ts index 6a84c06..7e941b5 100644 --- a/common/entities/Error.ts +++ b/common/entities/Error.ts @@ -11,13 +11,14 @@ export enum ErrorCodes { GENERAL_ERROR = 7, THUMBNAIL_GENERATION_ERROR = 8, - SERVER_ERROR = 9, + PERSON_ERROR = 9, + SERVER_ERROR = 10, - USER_MANAGEMENT_DISABLED = 10, + USER_MANAGEMENT_DISABLED = 11, - INPUT_ERROR = 11, + INPUT_ERROR = 12, - SETTINGS_ERROR = 12 + SETTINGS_ERROR = 13 } export class ErrorDTO { diff --git a/common/entities/PersonDTO.ts b/common/entities/PersonDTO.ts index 736eedf..711d819 100644 --- a/common/entities/PersonDTO.ts +++ b/common/entities/PersonDTO.ts @@ -3,10 +3,12 @@ export interface PersonDTO { name: string; count: number; readyThumbnail: boolean; + isFavourite: boolean; } export class Person implements PersonDTO { + isFavourite: boolean; count: number; id: number; name: string; diff --git a/frontend/app/ui/faces/face/face.component.css b/frontend/app/ui/faces/face/face.component.css index ce672cd..657c3cb 100644 --- a/frontend/app/ui/faces/face/face.component.css +++ b/frontend/app/ui/faces/face/face.component.css @@ -1,3 +1,25 @@ +.star { + margin: 2px; + color: #888; + cursor: default; + +} + +.star.favourite { + color: white; +} + +.star.clickable { + cursor: pointer; + transition: all .05s ease-in-out; + transform: scale(1.0, 1.0); +} + +.star.clickable:hover { + transform: scale(1.4, 1.4); +} + + a { position: relative; } @@ -51,6 +73,7 @@ a:hover .photo-container { } .person-name { + display: inline-block; width: 180px; white-space: normal; } diff --git a/frontend/app/ui/faces/face/face.component.html b/frontend/app/ui/faces/face/face.component.html index dabf7eb..3d2a134 100644 --- a/frontend/app/ui/faces/face/face.component.html +++ b/frontend/app/ui/faces/face/face.component.html @@ -1,11 +1,11 @@ -
+
@@ -18,7 +18,10 @@
-
{{person.name}} ({{person.count}})
+ + {{person.name}} ({{person.count}})
diff --git a/frontend/app/ui/faces/face/face.component.ts b/frontend/app/ui/faces/face/face.component.ts index ca66908..e6bfcca 100644 --- a/frontend/app/ui/faces/face/face.component.ts +++ b/frontend/app/ui/faces/face/face.component.ts @@ -4,6 +4,9 @@ import {PersonDTO} from '../../../../../common/entities/PersonDTO'; import {SearchTypes} from '../../../../../common/entities/AutoCompleteItem'; import {DomSanitizer} from '@angular/platform-browser'; import {PersonThumbnail, ThumbnailManagerService} from '../../gallery/thumbnailManager.service'; +import {FacesService} from '../faces.service'; +import {AuthenticationService} from '../../../model/network/authentication.service'; +import {Config} from '../../../../../common/config/public/Config'; @Component({ selector: 'app-face', @@ -19,10 +22,16 @@ export class FaceComponent implements OnInit, OnDestroy { SearchTypes = SearchTypes; constructor(private thumbnailService: ThumbnailManagerService, - private _sanitizer: DomSanitizer) { + private _sanitizer: DomSanitizer, + private faceService: FacesService, + public authenticationService: AuthenticationService) { } + get CanUpdate(): boolean { + return this.authenticationService.user.getValue().role >= Config.Client.Faces.writeAccessMinRole; + } + ngOnInit() { this.thumbnail = this.thumbnailService.getPersonThumbnail(this.person); @@ -42,5 +51,11 @@ export class FaceComponent implements OnInit, OnDestroy { } } + async toggleFavourite($event: MouseEvent) { + $event.preventDefault(); + $event.stopPropagation(); + await this.faceService.setFavourite(this.person, !this.person.isFavourite).catch(console.error); + this.faceService.getPersons(); + } } diff --git a/frontend/app/ui/faces/faces.component.html b/frontend/app/ui/faces/faces.component.html index 5e0ddb0..fb2f8d4 100644 --- a/frontend/app/ui/faces/faces.component.html +++ b/frontend/app/ui/faces/faces.component.html @@ -1,8 +1,12 @@
- + +
+
diff --git a/frontend/app/ui/faces/faces.component.ts b/frontend/app/ui/faces/faces.component.ts index 646ea6b..c9f3c2f 100644 --- a/frontend/app/ui/faces/faces.component.ts +++ b/frontend/app/ui/faces/faces.component.ts @@ -1,7 +1,9 @@ import {Component, ElementRef, OnInit, ViewChild} from '@angular/core'; import {FacesService} from './faces.service'; import {QueryService} from '../../model/query.service'; - +import {map} from 'rxjs/operators'; +import {PersonDTO} from '../../../../common/entities/PersonDTO'; +import {Observable} from 'rxjs/Observable'; @Component({ selector: 'app-faces', @@ -11,10 +13,24 @@ import {QueryService} from '../../model/query.service'; export class FacesComponent implements OnInit { @ViewChild('container') container: ElementRef; public size: number; + favourites: Observable; + nonFavourites: Observable; constructor(public facesService: FacesService, public queryService: QueryService) { this.facesService.getPersons().catch(console.error); + const personCmp = (p1: PersonDTO, p2: PersonDTO) => { + return p1.name.localeCompare(p2.name); + }; + this.favourites = this.facesService.persons.pipe( + map(value => value.filter(p => p.isFavourite) + .sort(personCmp)) + ); + this.nonFavourites = this.facesService.persons.pipe( + map(value => + value.filter(p => !p.isFavourite) + .sort(personCmp)) + ); } diff --git a/frontend/app/ui/faces/faces.service.ts b/frontend/app/ui/faces/faces.service.ts index a4f66a7..e6677f0 100644 --- a/frontend/app/ui/faces/faces.service.ts +++ b/frontend/app/ui/faces/faces.service.ts @@ -6,11 +6,22 @@ import {PersonDTO} from '../../../../common/entities/PersonDTO'; @Injectable() export class FacesService { - public persons: BehaviorSubject; constructor(private networkService: NetworkService) { - this.persons = new BehaviorSubject(null); + this.persons = new BehaviorSubject([]); + } + + public async setFavourite(person: PersonDTO, isFavourite: boolean): Promise { + const updated = await this.networkService.postJson('/person/' + person.name, {isFavourite: isFavourite}); + const updatesList = this.persons.getValue(); + for (let i = 0; i < updatesList.length; i++) { + if (updatesList[i].id === updated.id) { + updatesList[i] = updated; + this.persons.next(updatesList); + return; + } + } } public async getPersons() { diff --git a/frontend/app/ui/settings/settings.service.ts b/frontend/app/ui/settings/settings.service.ts index 1f15486..23073c4 100644 --- a/frontend/app/ui/settings/settings.service.ts +++ b/frontend/app/ui/settings/settings.service.ts @@ -69,7 +69,8 @@ export class SettingsService { }, Faces: { enabled: true, - keywordsToPersons: true + keywordsToPersons: true, + writeAccessMinRole: UserRoles.Admin }, urlBase: '', publicUrl: '',