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

adding favourite option to faces

This commit is contained in:
Patrik J. Braun 2019-03-03 21:17:42 +01:00
parent 8106646739
commit 5695f74e29
17 changed files with 167 additions and 24 deletions

View File

@ -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));
}
}

View File

@ -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<PersonEntry[]>;
@ -14,4 +15,6 @@ export interface IPersonManager {
keywordsToPerson(media: MediaDTO[]): Promise<void>;
updateCounts(): Promise<void>;
updatePerson(name: string, partialPerson: PersonDTO): Promise<PersonEntry>;
}

View File

@ -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<PersonEntry[]> {
getAll(): Promise<any[]> {
throw new Error('Method not implemented.');
}
@ -27,4 +28,8 @@ export class PersonManager implements IPersonManager {
updateCounts(): Promise<void> {
throw new Error('not supported by memory DB');
}
updatePerson(name: string, partialPerson: PersonDTO): Promise<any> {
throw new Error('not supported by memory DB');
}
}

View File

@ -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<PersonEntry> {
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<PhotoDTO> {
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;

View File

@ -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[];

View File

@ -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

View File

@ -1 +1 @@
export const DataStructureVersion = 11;
export const DataStructureVersion = 12;

View File

@ -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,

View File

@ -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 {

View File

@ -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;

View File

@ -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;
}

View File

@ -1,11 +1,11 @@
<a class="button btn btn-secondary"
[routerLink]="['/search', person.name, {type: SearchTypes[SearchTypes.person]}]"
<a [routerLink]="['/search', person.name, {type: SearchTypes[SearchTypes.person]}]"
style="display: inline-block;">
<div class="photo-container"
[style.width.px]="size"
[style.height.px]="size">
<div class="photo"
*ngIf="thumbnail && thumbnail.Available"
[style.background-image]="getSanitizedThUrl()"></div>
@ -18,7 +18,10 @@
<!--Info box -->
<div class="info">
<div class="person-name">{{person.name}} ({{person.count}})</div>
<span (click)="CanUpdate && toggleFavourite($event)"
class="star oi oi-star"
[ngClass]="{'favourite':person.isFavourite, 'clickable':CanUpdate}"></span>
{{person.name}} ({{person.count}})
</div>
</a>

View File

@ -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();
}
}

View File

@ -1,8 +1,12 @@
<app-frame>
<div body #container class="container-fluid">
<app-face *ngFor="let person of facesService.persons.value"
[person]="person"
[size]="size"></app-face>
<app-face *ngFor="let person of favourites | async"
[person]="person"
[size]="size"></app-face>
<hr/>
<app-face *ngFor="let person of nonFavourites | async"
[person]="person"
[size]="size"></app-face>
</div>
</app-frame>

View File

@ -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<PersonDTO[]>;
nonFavourites: Observable<PersonDTO[]>;
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))
);
}

View File

@ -6,11 +6,22 @@ import {PersonDTO} from '../../../../common/entities/PersonDTO';
@Injectable()
export class FacesService {
public persons: BehaviorSubject<PersonDTO[]>;
constructor(private networkService: NetworkService) {
this.persons = new BehaviorSubject<PersonDTO[]>(null);
this.persons = new BehaviorSubject<PersonDTO[]>([]);
}
public async setFavourite(person: PersonDTO, isFavourite: boolean): Promise<void> {
const updated = await this.networkService.postJson<PersonDTO>('/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() {

View File

@ -69,7 +69,8 @@ export class SettingsService {
},
Faces: {
enabled: true,
keywordsToPersons: true
keywordsToPersons: true,
writeAccessMinRole: UserRoles.Admin
},
urlBase: '',
publicUrl: '',