adding persons support for frontend
@ -113,7 +113,7 @@ export class ObjectManagerRepository {
|
||||
const UserManager = require('./sql/UserManager').UserManager;
|
||||
const SearchManager = require('./sql/SearchManager').SearchManager;
|
||||
const SharingManager = require('./sql/SharingManager').SharingManager;
|
||||
const IndexingTaskManager = require('./sql/IndexingManager').IndexingTaskManager;
|
||||
const IndexingTaskManager = require('./sql/IndexingTaskManager').IndexingTaskManager;
|
||||
const IndexingManager = require('./sql/IndexingManager').IndexingManager;
|
||||
const PersonManager = require('./sql/PersonManager').PersonManager;
|
||||
ObjectManagerRepository.getInstance().GalleryManager = new GalleryManager();
|
||||
|
@ -6,6 +6,8 @@ import {PhotoEntity} from './enitites/PhotoEntity';
|
||||
import {DirectoryEntity} from './enitites/DirectoryEntity';
|
||||
import {MediaEntity} from './enitites/MediaEntity';
|
||||
import {VideoEntity} from './enitites/VideoEntity';
|
||||
import {PersonEntry} from './enitites/PersonEntry';
|
||||
import {FaceRegionEntry} from './enitites/FaceRegionEntry';
|
||||
|
||||
export class SearchManager implements ISearchManager {
|
||||
|
||||
@ -29,7 +31,7 @@ export class SearchManager implements ISearchManager {
|
||||
let result: AutoCompleteItem[] = [];
|
||||
const photoRepository = connection.getRepository(PhotoEntity);
|
||||
const videoRepository = connection.getRepository(VideoEntity);
|
||||
const mediaRepository = connection.getRepository(MediaEntity);
|
||||
const personRepository = connection.getRepository(PersonEntry);
|
||||
const directoryRepository = connection.getRepository(DirectoryEntity);
|
||||
|
||||
|
||||
@ -45,6 +47,14 @@ export class SearchManager implements ISearchManager {
|
||||
.filter(k => k.toLowerCase().indexOf(text.toLowerCase()) !== -1), SearchTypes.keyword));
|
||||
});
|
||||
|
||||
result = result.concat(this.encapsulateAutoComplete((await personRepository
|
||||
.createQueryBuilder('person')
|
||||
.select('DISTINCT(person.name)')
|
||||
.where('person.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'})
|
||||
.limit(5)
|
||||
.getRawMany())
|
||||
.map(r => r.name), SearchTypes.person));
|
||||
|
||||
(await photoRepository
|
||||
.createQueryBuilder('photo')
|
||||
.select('photo.metadata.positionData.country as country, ' +
|
||||
@ -112,16 +122,19 @@ export class SearchManager implements ISearchManager {
|
||||
resultOverflow: false
|
||||
};
|
||||
|
||||
let repostiroy = connection.getRepository(MediaEntity);
|
||||
let repository = connection.getRepository(MediaEntity);
|
||||
const faceRepository = connection.getRepository(FaceRegionEntry);
|
||||
|
||||
if (searchType === SearchTypes.photo) {
|
||||
repostiroy = connection.getRepository(PhotoEntity);
|
||||
repository = connection.getRepository(PhotoEntity);
|
||||
} else if (searchType === SearchTypes.video) {
|
||||
repostiroy = connection.getRepository(VideoEntity);
|
||||
repository = connection.getRepository(VideoEntity);
|
||||
}
|
||||
|
||||
const query = repostiroy.createQueryBuilder('media')
|
||||
.innerJoinAndSelect('media.directory', 'directory')
|
||||
const query = repository.createQueryBuilder('media')
|
||||
.leftJoinAndSelect('media.directory', 'directory')
|
||||
.leftJoin('media.metadata.faces', 'faces')
|
||||
.leftJoin('faces.person', 'person')
|
||||
.orderBy('media.metadata.creationDate', 'ASC');
|
||||
|
||||
|
||||
@ -136,6 +149,9 @@ export class SearchManager implements ISearchManager {
|
||||
if (!searchType || searchType === SearchTypes.photo) {
|
||||
query.orWhere('media.metadata.caption LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'});
|
||||
}
|
||||
if (!searchType || searchType === SearchTypes.person) {
|
||||
query.orWhere('person.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'});
|
||||
}
|
||||
|
||||
if (!searchType || searchType === SearchTypes.position) {
|
||||
query.orWhere('media.metadata.positionData.country LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'})
|
||||
@ -147,9 +163,20 @@ export class SearchManager implements ISearchManager {
|
||||
query.orWhere('media.metadata.keywords LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'});
|
||||
}
|
||||
|
||||
result.media = await query
|
||||
.limit(2001)
|
||||
.getMany();
|
||||
|
||||
result.media = (await query
|
||||
.limit(5000).getMany()).slice(0, 2001);
|
||||
|
||||
for (let i = 0; i < result.media.length; i++) {
|
||||
const faces = (await faceRepository
|
||||
.createQueryBuilder('faces')
|
||||
.leftJoinAndSelect('faces.person', 'person')
|
||||
.where('faces.media = :media', {media: result.media[i].id})
|
||||
.getMany()).map(fE => ({name: fE.person.name, box: fE.box}));
|
||||
if (faces.length > 0) {
|
||||
result.media[i].metadata.faces = faces;
|
||||
}
|
||||
}
|
||||
|
||||
if (result.media.length > 2000) {
|
||||
result.resultOverflow = true;
|
||||
@ -181,6 +208,8 @@ export class SearchManager implements ISearchManager {
|
||||
resultOverflow: false
|
||||
};
|
||||
|
||||
const faceRepository = connection.getRepository(FaceRegionEntry);
|
||||
|
||||
result.media = await connection
|
||||
.getRepository(MediaEntity)
|
||||
.createQueryBuilder('media')
|
||||
@ -191,10 +220,23 @@ export class SearchManager implements ISearchManager {
|
||||
.orWhere('media.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'})
|
||||
.orWhere('media.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'})
|
||||
.orWhere('media.metadata.caption LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'})
|
||||
.orWhere('person.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'})
|
||||
.innerJoinAndSelect('media.directory', 'directory')
|
||||
.leftJoin('media.metadata.faces', 'faces')
|
||||
.leftJoin('faces.person', 'person')
|
||||
.limit(10)
|
||||
.getMany();
|
||||
|
||||
for (let i = 0; i < result.media.length; i++) {
|
||||
const faces = (await faceRepository
|
||||
.createQueryBuilder('faces')
|
||||
.leftJoinAndSelect('faces.person', 'person')
|
||||
.where('faces.media = :media', {media: result.media[i].id})
|
||||
.getMany()).map(fE => ({name: fE.person.name, box: fE.box}));
|
||||
if (faces.length > 0) {
|
||||
result.media[i].metadata.faces = faces;
|
||||
}
|
||||
}
|
||||
|
||||
result.directories = await connection
|
||||
.getRepository(DirectoryEntity)
|
||||
|
@ -4,7 +4,7 @@ import {Config} from '../../../common/config/private/Config';
|
||||
import {Logger} from '../../Logger';
|
||||
import * as fs from 'fs';
|
||||
import * as sizeOf from 'image-size';
|
||||
import {OrientationTypes, ExifParserFactory} from 'ts-exif-parser';
|
||||
import {ExifParserFactory, OrientationTypes} from 'ts-exif-parser';
|
||||
import {IptcParser} from 'ts-node-iptc';
|
||||
import {FFmpegFactory} from '../FFmpegFactory';
|
||||
import {FfprobeData} from 'fluent-ffmpeg';
|
||||
@ -147,10 +147,16 @@ export class MetadataLoader {
|
||||
|
||||
try {
|
||||
const iptcData = IptcParser.parse(data);
|
||||
if (iptcData.country_or_primary_location_name || iptcData.province_or_state || iptcData.city) {
|
||||
if (iptcData.country_or_primary_location_name) {
|
||||
metadata.positionData = metadata.positionData || {};
|
||||
metadata.positionData.country = iptcData.country_or_primary_location_name.replace(/\0/g, '').trim();
|
||||
}
|
||||
if (iptcData.province_or_state) {
|
||||
metadata.positionData = metadata.positionData || {};
|
||||
metadata.positionData.state = iptcData.province_or_state.replace(/\0/g, '').trim();
|
||||
}
|
||||
if (iptcData.city) {
|
||||
metadata.positionData = metadata.positionData || {};
|
||||
metadata.positionData.city = iptcData.city.replace(/\0/g, '').trim();
|
||||
}
|
||||
if (iptcData.caption) {
|
||||
@ -160,7 +166,7 @@ export class MetadataLoader {
|
||||
metadata.creationDate = <number>(iptcData.date_time ? iptcData.date_time.getTime() : metadata.creationDate);
|
||||
|
||||
} catch (err) {
|
||||
// Logger.debug(LOG_TAG, 'Error parsing iptc data', fullPath, err);
|
||||
Logger.debug(LOG_TAG, 'Error parsing iptc data', fullPath, err);
|
||||
}
|
||||
|
||||
metadata.creationDate = metadata.creationDate || 0;
|
||||
|
@ -1,9 +1,10 @@
|
||||
export enum SearchTypes {
|
||||
directory = 1,
|
||||
keyword = 2,
|
||||
position = 3,
|
||||
photo = 4,
|
||||
video = 5
|
||||
person = 2,
|
||||
keyword = 3,
|
||||
position = 5,
|
||||
photo = 6,
|
||||
video = 7
|
||||
}
|
||||
|
||||
export class AutoCompleteItem {
|
||||
|
Before Width: | Height: | Size: 695 KiB After Width: | Height: | Size: 696 KiB |
Before Width: | Height: | Size: 883 KiB After Width: | Height: | Size: 884 KiB |
Before Width: | Height: | Size: 452 KiB After Width: | Height: | Size: 453 KiB |
Before Width: | Height: | Size: 442 KiB After Width: | Height: | Size: 445 KiB |
Before Width: | Height: | Size: 528 KiB After Width: | Height: | Size: 530 KiB |
@ -92,7 +92,7 @@ a {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.video-indicator{
|
||||
.video-indicator {
|
||||
font-size: large;
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
@ -102,12 +102,16 @@ a {
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
color: white;
|
||||
transition: background-color .3s ease-out;
|
||||
-moz-transition: background-color .3s ease-out;
|
||||
-moz-transition: background-color .3s ease-out;
|
||||
-webkit-transition: background-color .3s ease-out;
|
||||
-o-transition: background-color .3s ease-out;
|
||||
-ms-transition: background-color .3s ease-out;
|
||||
-o-transition: background-color .3s ease-out;
|
||||
-ms-transition: background-color .3s ease-out;
|
||||
}
|
||||
|
||||
.photo-container:hover .video-indicator{
|
||||
.photo-container:hover .video-indicator {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
}
|
||||
|
||||
.photo-keywords .oi-person{
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
@ -36,12 +36,18 @@
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<div class="photo-keywords" *ngIf="gridPhoto.media.metadata.keywords && gridPhoto.media.metadata.keywords.length">
|
||||
<ng-template ngFor let-keyword [ngForOf]="gridPhoto.media.metadata.keywords" let-last="last">
|
||||
<div class="photo-keywords" *ngIf="keywords">
|
||||
<ng-template ngFor let-keyword [ngForOf]="keywords" let-last="last">
|
||||
<a *ngIf="searchEnabled"
|
||||
[routerLink]="['/search', keyword, {type: SearchTypes[SearchTypes.keyword]}]">#{{keyword}}</a>
|
||||
<span *ngIf="!searchEnabled">#{{keyword}}</span>
|
||||
<ng-template [ngIf]="!last">, </ng-template>
|
||||
[routerLink]="['/search', keyword.value, {type: SearchTypes[keyword.type]}]" [ngSwitch]="keyword.type">
|
||||
<ng-template [ngSwitchCase]="SearchTypes.keyword">#</ng-template><!--
|
||||
--><ng-template [ngSwitchCase]="SearchTypes.person"><span class="oi oi-person"></span></ng-template><!--
|
||||
-->{{keyword.value}}</a>
|
||||
<span *ngIf="!searchEnabled" [ngSwitch]="keyword.type">
|
||||
<ng-template [ngSwitchCase]="SearchTypes.keyword">#</ng-template><!--
|
||||
--><ng-template [ngSwitchCase]="SearchTypes.person"><span class="oi oi-person"></span></ng-template><!--
|
||||
-->{{keyword.value}}</span>
|
||||
<ng-template [ngIf]="!last">,</ng-template>
|
||||
</ng-template>
|
||||
|
||||
</div>
|
||||
|
@ -5,9 +5,8 @@ import {SearchTypes} from '../../../../../common/entities/AutoCompleteItem';
|
||||
import {RouterLink} from '@angular/router';
|
||||
import {Thumbnail, ThumbnailManagerService} from '../../thumbnailManager.service';
|
||||
import {Config} from '../../../../../common/config/public/Config';
|
||||
import {AnimationBuilder} from '@angular/animations';
|
||||
import {PageHelper} from '../../../model/page.helper';
|
||||
import {PhotoDTO} from '../../../../../common/entities/PhotoDTO';
|
||||
import {PhotoDTO, PhotoMetadata} from '../../../../../common/entities/PhotoDTO';
|
||||
|
||||
@Component({
|
||||
selector: 'app-gallery-grid-photo',
|
||||
@ -22,6 +21,7 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy {
|
||||
@ViewChild('photoContainer') container: ElementRef;
|
||||
|
||||
thumbnail: Thumbnail;
|
||||
keywords: { value: string, type: SearchTypes }[] = null;
|
||||
infoBar = {
|
||||
marginTop: 0,
|
||||
visible: false,
|
||||
@ -34,17 +34,40 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy {
|
||||
|
||||
wasInView: boolean = null;
|
||||
|
||||
constructor(private thumbnailService: ThumbnailManagerService,
|
||||
private _animationBuilder: AnimationBuilder) {
|
||||
constructor(private thumbnailService: ThumbnailManagerService) {
|
||||
this.SearchTypes = SearchTypes;
|
||||
this.searchEnabled = Config.Client.Search.enabled;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.thumbnail = this.thumbnailService.getThumbnail(this.gridPhoto);
|
||||
get ScrollListener(): boolean {
|
||||
return !this.thumbnail.Available && !this.thumbnail.Error;
|
||||
}
|
||||
|
||||
|
||||
get Title(): string {
|
||||
if (Config.Client.Other.captionFirstNaming === false) {
|
||||
return this.gridPhoto.media.name;
|
||||
}
|
||||
if ((<PhotoDTO>this.gridPhoto.media).metadata.caption) {
|
||||
if ((<PhotoDTO>this.gridPhoto.media).metadata.caption.length > 20) {
|
||||
return (<PhotoDTO>this.gridPhoto.media).metadata.caption.substring(0, 17) + '...';
|
||||
}
|
||||
return (<PhotoDTO>this.gridPhoto.media).metadata.caption;
|
||||
}
|
||||
return this.gridPhoto.media.name;
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.thumbnail = this.thumbnailService.getThumbnail(this.gridPhoto);
|
||||
const metadata = this.gridPhoto.media.metadata as PhotoMetadata;
|
||||
if ((metadata.keywords && metadata.keywords.length > 0) ||
|
||||
(metadata.faces && metadata.faces.length > 0)) {
|
||||
this.keywords = (metadata.faces || []).map(f => ({value: f.name, type: SearchTypes.person}))
|
||||
.concat((metadata.keywords || []).map(k => ({value: k, type: SearchTypes.keyword})));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.thumbnail.destroy();
|
||||
|
||||
@ -53,16 +76,11 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
isInView(): boolean {
|
||||
return PageHelper.ScrollY < this.container.nativeElement.offsetTop + this.container.nativeElement.clientHeight
|
||||
&& PageHelper.ScrollY + window.innerHeight > this.container.nativeElement.offsetTop;
|
||||
}
|
||||
|
||||
get ScrollListener(): boolean {
|
||||
return !this.thumbnail.Available && !this.thumbnail.Error;
|
||||
}
|
||||
|
||||
onScroll() {
|
||||
if (this.thumbnail.Available === true || this.thumbnail.Error === true) {
|
||||
return;
|
||||
@ -74,7 +92,6 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
getPositionText(): string {
|
||||
if (!this.gridPhoto || !this.gridPhoto.isPhoto()) {
|
||||
return '';
|
||||
@ -84,7 +101,6 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy {
|
||||
(<PhotoDTO>this.gridPhoto.media).metadata.positionData.country;
|
||||
}
|
||||
|
||||
|
||||
mouseOver() {
|
||||
this.infoBar.visible = true;
|
||||
if (this.animationTimer != null) {
|
||||
@ -124,19 +140,6 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy {
|
||||
|
||||
}
|
||||
|
||||
get Title(): string {
|
||||
if (Config.Client.Other.captionFirstNaming === false) {
|
||||
return this.gridPhoto.media.name;
|
||||
}
|
||||
if ((<PhotoDTO>this.gridPhoto.media).metadata.caption) {
|
||||
if ((<PhotoDTO>this.gridPhoto.media).metadata.caption.length > 20) {
|
||||
return (<PhotoDTO>this.gridPhoto.media).metadata.caption.substring(0, 17) + '...';
|
||||
}
|
||||
return (<PhotoDTO>this.gridPhoto.media).metadata.caption;
|
||||
}
|
||||
return this.gridPhoto.media.name;
|
||||
}
|
||||
|
||||
/*
|
||||
onImageLoad() {
|
||||
this.loading.show = false;
|
||||
|
@ -15,6 +15,7 @@
|
||||
<span *ngSwitchCase="SearchTypes.video" class="oi oi-video"></span>
|
||||
<span *ngSwitchCase="SearchTypes.directory" class="oi oi-folder"></span>
|
||||
<span *ngSwitchCase="SearchTypes.keyword" class="oi oi-tag"></span>
|
||||
<span *ngSwitchCase="SearchTypes.person" class="oi oi-person"></span>
|
||||
<span *ngSwitchCase="SearchTypes.position" class="oi oi-map-marker"></span>
|
||||
</span>
|
||||
<strong> {{searchResult.searchText}}</strong>
|
||||
|
@ -25,6 +25,7 @@
|
||||
<span *ngSwitchCase="SearchTypes.video" class="oi oi-video"></span>
|
||||
<span *ngSwitchCase="SearchTypes.directory" class="oi oi-folder"></span>
|
||||
<span *ngSwitchCase="SearchTypes.keyword" class="oi oi-tag"></span>
|
||||
<span *ngSwitchCase="SearchTypes.person" class="oi oi-person"></span>
|
||||
<span *ngSwitchCase="SearchTypes.position" class="oi oi-map-marker"></span>
|
||||
</span>
|
||||
{{item.preText}}<strong>{{item.highLightText}}</strong>{{item.postText}}
|
||||
|