1
0
mirror of https://github.com/xuthus83/pigallery2.git synced 2025-01-14 14:43:17 +08:00

implementing search

This commit is contained in:
Braun Patrik 2016-05-09 21:43:52 +02:00
parent 225d2b6a8c
commit c93c1aba2d
18 changed files with 218 additions and 40 deletions

View File

@ -7,6 +7,8 @@ import {Config} from "../config/Config";
import {ObjectManagerRepository} from "../model/ObjectManagerRepository"; import {ObjectManagerRepository} from "../model/ObjectManagerRepository";
import {AutoCompleteItem} from "../../common/entities/AutoCompleteItem"; import {AutoCompleteItem} from "../../common/entities/AutoCompleteItem";
import {ContentWrapper} from "../../common/entities/ConentWrapper"; import {ContentWrapper} from "../../common/entities/ConentWrapper";
import {SearchResult} from "../../common/entities/SearchResult";
import {Photo} from "../../common/entities/Photo";
export class GalleryMWs { export class GalleryMWs {
@ -27,12 +29,19 @@ export class GalleryMWs {
if (err || !directory) { if (err || !directory) {
return next(new Error(ErrorCodes.GENERAL_ERROR, err)); return next(new Error(ErrorCodes.GENERAL_ERROR, err));
} }
//remove cyclic reference
directory.photos.forEach((photo:Photo) => {
photo.directory = null;
});
req.resultPipe = new ContentWrapper(directory, null); req.resultPipe = new ContentWrapper(directory, null);
return next(); return next();
}); });
} }
public static loadImage(req:Request, res:Response, next:NextFunction) { public static loadImage(req:Request, res:Response, next:NextFunction) {
if (!(req.params.imagePath)) { if (!(req.params.imagePath)) {
return next(); return next();
@ -49,14 +58,29 @@ export class GalleryMWs {
public static search(req:Request, res:Response, next:NextFunction) { public static search(req:Request, res:Response, next:NextFunction) {
//TODO: implement
return next(new Error(ErrorCodes.GENERAL_ERROR)); ObjectManagerRepository.getInstance().getSearchManager().search(req.params.text, (err, result:SearchResult) => {
if (err || !result) {
return next(new Error(ErrorCodes.GENERAL_ERROR, err));
}
req.resultPipe = new ContentWrapper(null, result);
return next();
});
} }
public static instantSearch(req:Request, res:Response, next:NextFunction) { public static instantSearch(req:Request, res:Response, next:NextFunction) {
//TODO: implement if (!(req.params.text)) {
return next(new Error(ErrorCodes.GENERAL_ERROR)); return next();
}
ObjectManagerRepository.getInstance().getSearchManager().instantSearch(req.params.text, (err, result:SearchResult) => {
if (err || !result) {
return next(new Error(ErrorCodes.GENERAL_ERROR, err));
}
req.resultPipe = new ContentWrapper(null, result);
return next();
});
} }
public static autocomplete(req:Request, res:Response, next:NextFunction) { public static autocomplete(req:Request, res:Response, next:NextFunction) {

View File

@ -30,7 +30,7 @@ export class DiskManager {
if (DiskManager.isImage(fullFilePath)) { if (DiskManager.isImage(fullFilePath)) {
let dimensions = sizeOf(fullFilePath); let dimensions = sizeOf(fullFilePath);
directory.photos.push(new Photo(1, file, dimensions.width, dimensions.height)); directory.photos.push(new Photo(1, file, directory, dimensions.width, dimensions.height));
} }
} }

View File

@ -1,4 +1,7 @@
import {AutoCompleteItem} from "../../common/entities/AutoCompleteItem"; import {AutoCompleteItem} from "../../common/entities/AutoCompleteItem";
import {SearchResult} from "../../common/entities/SearchResult";
export interface ISearchManager { export interface ISearchManager {
autocomplete(text, cb:(error:any, result:Array<AutoCompleteItem>) => void); autocomplete(text, cb:(error:any, result:Array<AutoCompleteItem>) => void);
search(text, cb:(error:any, result:SearchResult) => void);
instantSearch(text, cb:(error:any, result:SearchResult) => void);
} }

View File

@ -1,5 +1,6 @@
import {AutoCompleteItem} from "../../../common/entities/AutoCompleteItem"; import {AutoCompleteItem} from "../../../common/entities/AutoCompleteItem";
import {ISearchManager} from "../ISearchManager"; import {ISearchManager} from "../ISearchManager";
import {SearchResult} from "../../../common/entities/SearchResult";
export class SearchManager implements ISearchManager { export class SearchManager implements ISearchManager {
@ -8,5 +9,13 @@ export class SearchManager implements ISearchManager {
throw new Error("not implemented"); throw new Error("not implemented");
} }
search(text, cb:(error:any, result:SearchResult) => void) {
throw new Error("not implemented");
}
instantSearch(text, cb:(error:any, result:SearchResult) => void) {
throw new Error("not implemented");
}
} }

View File

@ -5,6 +5,7 @@ import {DiskManager} from "../DiskManger";
import {Utils} from "../../../common/Utils"; import {Utils} from "../../../common/Utils";
import {DirectoryModel} from "./entities/DirectoryModel"; import {DirectoryModel} from "./entities/DirectoryModel";
import {PhotoModel} from "./entities/PhotoModel"; import {PhotoModel} from "./entities/PhotoModel";
import {Photo} from "../../../common/entities/Photo";
export class MongoGalleryManager implements IGalleryManager { export class MongoGalleryManager implements IGalleryManager {
@ -22,7 +23,7 @@ export class MongoGalleryManager implements IGalleryManager {
if (err || !res) { if (err || !res) {
return this.indexDirectory(relativeDirectoryName, cb); return this.indexDirectory(relativeDirectoryName, cb);
} }
return cb(err, res); return cb(err, this.modelToEntity(res));
}); });
} }
@ -46,12 +47,38 @@ export class MongoGalleryManager implements IGalleryManager {
}); });
scannedDirectory.photos = arr; scannedDirectory.photos = arr;
DirectoryModel.create(scannedDirectory, (err)=> { DirectoryModel.create(scannedDirectory, (err, savedDir)=> {
return cb(err, scannedDirectory); scannedDirectory.photos.forEach((value:any) => {
value['directory'] = savedDir;
value.save();
});
return cb(err, this.modelToEntity(scannedDirectory));
}); });
}); });
} }
private modelToEntity(directroy:any):Directory {
console.log("modelToEntity");
// console.log(directroy);
let directoryEntity = new Directory(directroy._id);
Utils.updateKeys(directoryEntity, directroy);
directroy.photos.forEach((photo) => {
let photoEntity = new Photo(null, null, null, null, null);
Utils.updateKeys(photoEntity, photo);
console.log(photoEntity);
directoryEntity.photos.push(photoEntity);
});
directroy.directories.forEach((dir) => {
let dirEntity = new Directory(null, null, null, null, null, null);
Utils.updateKeys(dirEntity, dir);
console.log(dir);
console.log(dirEntity);
directoryEntity.directories.push(dirEntity);
});
return directoryEntity;
}
} }

View File

@ -2,6 +2,7 @@ import {AutoCompleteItem, AutoCompeleteTypes} from "../../../common/entities/Aut
import {ISearchManager} from "../ISearchManager"; import {ISearchManager} from "../ISearchManager";
import {DirectoryModel} from "./entities/DirectoryModel"; import {DirectoryModel} from "./entities/DirectoryModel";
import {PhotoModel} from "./entities/PhotoModel"; import {PhotoModel} from "./entities/PhotoModel";
import {SearchResult} from "../../../common/entities/SearchResult";
export class MongoSearchManager implements ISearchManager { export class MongoSearchManager implements ISearchManager {
@ -35,6 +36,70 @@ export class MongoSearchManager implements ISearchManager {
}); });
} }
search(text, cb:(error:any, result:SearchResult) => void) {
console.log("instantSearch: " + text);
let result:SearchResult = new SearchResult();
result.searchText = text;
PhotoModel.find({
name: {
$regex: text,
$options: "i"
}
}).populate('directory', 'name path').exec((err, res:Array<any>) => {
if (err || !res) {
return cb(err, null);
}
result.photos = res;
DirectoryModel.find({
name: {
$regex: text,
$options: "i"
}
}).select('name').exec((err, res:Array<any>) => {
if (err || !res) {
return cb(err, null);
}
result.directories = res;
return cb(null, result);
});
});
}
instantSearch(text, cb:(error:any, result:SearchResult) => void) {
console.log("instantSearch: " + text);
let result:SearchResult = new SearchResult();
result.searchText = text;
PhotoModel.find({
name: {
$regex: text,
$options: "i"
}
}).limit(10).populate('directory', 'name path').exec((err, res:Array<any>) => {
if (err || !res) {
return cb(err, null);
}
result.photos = res;
DirectoryModel.find({
name: {
$regex: text,
$options: "i"
}
}).limit(10).exec((err, res:Array<any>) => {
if (err || !res) {
return cb(err, null);
}
result.directories = res;
return cb(null, result);
});
});
}
private encapsulateAutoComplete(values:Array<string>, type:AutoCompeleteTypes) { private encapsulateAutoComplete(values:Array<string>, type:AutoCompeleteTypes) {
let res = []; let res = [];
values.forEach((value)=> { values.forEach((value)=> {

View File

@ -1,8 +1,15 @@
import {DatabaseManager} from "../DatabaseManager"; import {DatabaseManager} from "../DatabaseManager";
import {Schema} from "mongoose";
export var PhotoModel = DatabaseManager.getInstance().getModel('photo',{ export var PhotoModel = DatabaseManager.getInstance().getModel('photo',{
name:String, name:String,
width:Number, width:Number,
height:Number height: Number,
directory: {
type: Schema.Types.ObjectId,
ref: 'directory'
},
}); });

View File

@ -20,7 +20,7 @@ export class Utils {
} }
public static updateKeys(targetObject, sourceObject) { public static updateKeys(targetObject, sourceObject) {
Object.keys(sourceObject).forEach((key)=> { Object.keys(sourceObject).forEach((key)=> {
if (typeof targetObject[key] === "undefined") { if (typeof targetObject[key] === "undefined") {
return; return;
} }

View File

@ -1,14 +1,14 @@
import {Utils} from "../Utils"; import {Utils} from "../Utils";
import {Directory} from "./Directory"; import {Directory} from "./Directory";
export class Photo { export class Photo {
constructor(public id:number, public name:string, public width:number, public height:number) { constructor(public id?:number, public name?:string, public directory?:Directory, public width?:number, public height?:number) {
} }
public static getThumbnailPath(directory:Directory, photo:Photo) { public static getThumbnailPath(photo:Photo) {
return Utils.concatUrls("/api/gallery/content/", directory.path, directory.name, photo.name, "thumbnail"); return Utils.concatUrls("/api/gallery/content/", photo.directory.path, photo.directory.name, photo.name, "thumbnail");
} }
public static getPhotoPath(directory:Directory, photo:Photo) { public static getPhotoPath(photo:Photo) {
return Utils.concatUrls("/api/gallery/content/", directory.path, directory.name, photo.name); return Utils.concatUrls("/api/gallery/content/", photo.directory.path, photo.directory.name, photo.name);
} }
} }

View File

@ -2,11 +2,8 @@ import {Directory} from "./Directory";
import {Photo} from "./Photo"; import {Photo} from "./Photo";
export class SearchResult { export class SearchResult {
public searchText:string;
public directories:Array<Directory>; public directories:Array<Directory>;
public photos:Array<Photo>; public photos:Array<Photo>;
constructor(directories:Array<Directory>, photos:Array<Photo>) {
this.directories = directories;
this.photos = photos;
}
} }

View File

@ -7,6 +7,14 @@
<div *ngFor="let directory of _galleryService.content.directory.directories"> <div *ngFor="let directory of _galleryService.content.directory.directories">
<gallery-directory *ngIf="directory" [directory]="directory"></gallery-directory> <gallery-directory *ngIf="directory" [directory]="directory"></gallery-directory>
</div> </div>
<gallery-grid [directory]="_galleryService.content.directory" [lightbox]="lightbox"></gallery-grid> <gallery-grid [photos]="_galleryService.content.directory.photos" [lightbox]="lightbox"></gallery-grid>
</div>
<div body class="container" style="width: 100%; padding:0" *ngIf="_galleryService.content.searchResult">
<div> Searching for: {{_galleryService.content.searchResult.searchText}}</div>
<div *ngFor="let directory of _galleryService.content.searchResult.directories">
<gallery-directory *ngIf="directory" [directory]="directory"></gallery-directory>
</div>
<gallery-grid [photos]="_galleryService.content.searchResult.photos" [lightbox]="lightbox"></gallery-grid>
</div> </div>
</app-frame> </app-frame>

View File

@ -4,12 +4,16 @@ import {Injectable} from "@angular/core";
import {NetworkService} from "../model/network/network.service.ts"; import {NetworkService} from "../model/network/network.service.ts";
import {Message} from "../../../common/entities/Message"; import {Message} from "../../../common/entities/Message";
import {ContentWrapper} from "../../../common/entities/ConentWrapper"; import {ContentWrapper} from "../../../common/entities/ConentWrapper";
import {Photo} from "../../../common/entities/Photo";
import {Directory} from "../../../common/entities/Directory";
@Injectable() @Injectable()
export class GalleryService { export class GalleryService {
public content:ContentWrapper; public content:ContentWrapper;
private lastDirectory:Directory;
private searchId:any;
constructor(private _networkService:NetworkService) { constructor(private _networkService:NetworkService) {
this.content = new ContentWrapper(); this.content = new ContentWrapper();
} }
@ -18,13 +22,21 @@ export class GalleryService {
return this._networkService.getJson("/gallery/content/" + directoryName).then( return this._networkService.getJson("/gallery/content/" + directoryName).then(
(message:Message<ContentWrapper>) => { (message:Message<ContentWrapper>) => {
if (!message.error && message.result) { if (!message.error && message.result) {
message.result.directory.photos.forEach((photo:Photo) => {
photo.directory = message.result.directory;
});
this.lastDirectory = message.result.directory;
this.content = message.result; this.content = message.result;
} }
return message; return message;
}); });
} }
//TODO: cache
public search(text:string):Promise<Message<ContentWrapper>> { public search(text:string):Promise<Message<ContentWrapper>> {
if (text === null || text === '') {
return Promise.resolve(new Message(null, null));
}
return this._networkService.getJson("/gallery/search/" + text).then( return this._networkService.getJson("/gallery/search/" + text).then(
(message:Message<ContentWrapper>) => { (message:Message<ContentWrapper>) => {
if (!message.error && message.result) { if (!message.error && message.result) {
@ -34,7 +46,24 @@ export class GalleryService {
}); });
} }
//TODO: cache (together with normal search)
public instantSearch(text:string):Promise<Message<ContentWrapper>> { public instantSearch(text:string):Promise<Message<ContentWrapper>> {
if (text === null || text === '') {
this.content.directory = this.lastDirectory;
this.content.searchResult = null;
clearTimeout(this.searchId);
return Promise.resolve(new Message(null, null));
}
if (this.searchId != null) {
clearTimeout(this.searchId);
}
this.searchId = setTimeout(() => {
this.search(text);
this.searchId = null;
}, 3000); //TODO: set timeout to config
return this._networkService.getJson("/gallery/instant-search/" + text).then( return this._networkService.getJson("/gallery/instant-search/" + text).then(
(message:Message<ContentWrapper>) => { (message:Message<ContentWrapper>) => {
if (!message.error && message.result) { if (!message.error && message.result) {
@ -42,6 +71,7 @@ export class GalleryService {
} }
return message; return message;
}); });
} }
} }

View File

@ -3,7 +3,6 @@
*ngFor="let gridPhoto of photosToRender" *ngFor="let gridPhoto of photosToRender"
(click)="lightbox.show(gridPhoto.photo)" (click)="lightbox.show(gridPhoto.photo)"
[photo]="gridPhoto.photo" [photo]="gridPhoto.photo"
[directory]="directory"
[style.width.px]="gridPhoto.renderWidth" [style.width.px]="gridPhoto.renderWidth"
[style.height.px]="gridPhoto.renderHeight" [style.height.px]="gridPhoto.renderHeight"
[style.marginLeft.px]="IMAGE_MARGIN" [style.marginLeft.px]="IMAGE_MARGIN"

View File

@ -10,7 +10,6 @@ import {
QueryList, QueryList,
AfterViewInit AfterViewInit
} from "@angular/core"; } from "@angular/core";
import {Directory} from "../../../../common/entities/Directory";
import {Photo} from "../../../../common/entities/Photo"; import {Photo} from "../../../../common/entities/Photo";
import {GalleryPhotoComponent} from "../photo/photo.gallery.component"; import {GalleryPhotoComponent} from "../photo/photo.gallery.component";
import {GridRowBuilder} from "./GridRowBuilder"; import {GridRowBuilder} from "./GridRowBuilder";
@ -27,7 +26,7 @@ export class GalleryGridComponent implements OnChanges,AfterViewInit {
@ViewChild('gridContainer') gridContainer:ElementRef; @ViewChild('gridContainer') gridContainer:ElementRef;
@ViewChildren(GalleryPhotoComponent) gridPhotoQL:QueryList<GalleryPhotoComponent>; @ViewChildren(GalleryPhotoComponent) gridPhotoQL:QueryList<GalleryPhotoComponent>;
@Input() directory:Directory; @Input() photos:Array<Photo>;
@Input() lightbox:GalleryLightboxComponent; @Input() lightbox:GalleryLightboxComponent;
photosToRender:Array<GridPhoto> = []; photosToRender:Array<GridPhoto> = [];
@ -41,7 +40,7 @@ export class GalleryGridComponent implements OnChanges,AfterViewInit {
} }
ngOnChanges() { ngOnChanges() {
// this.renderPhotos(); this.renderPhotos();
} }
onResize() { onResize() {
@ -92,9 +91,9 @@ export class GalleryGridComponent implements OnChanges,AfterViewInit {
this.photosToRender = []; this.photosToRender = [];
let i = 0; let i = 0;
while (i < this.directory.photos.length) { while (i < this.photos.length) {
let photoRowBuilder = new GridRowBuilder(this.directory.photos, i, this.IMAGE_MARGIN, containerWidth); let photoRowBuilder = new GridRowBuilder(this.photos, i, this.IMAGE_MARGIN, containerWidth);
photoRowBuilder.addPhotos(this.TARGET_COL_COUNT); photoRowBuilder.addPhotos(this.TARGET_COL_COUNT);
photoRowBuilder.adjustRowHeightBetween(minRowHeight, maxRowHeight); photoRowBuilder.adjustRowHeightBetween(minRowHeight, maxRowHeight);

View File

@ -150,14 +150,14 @@ export class GalleryLightboxComponent {
if (!this.activePhoto) { if (!this.activePhoto) {
return ""; return "";
} }
return Photo.getPhotoPath(this.activePhoto.directory, this.activePhoto.photo); return Photo.getPhotoPath(this.activePhoto.photo);
} }
getThumbnailPath() { getThumbnailPath() {
if (!this.activePhoto) { if (!this.activePhoto) {
return ""; return "";
} }
return Photo.getThumbnailPath(this.activePhoto.directory, this.activePhoto.photo); return Photo.getThumbnailPath(this.activePhoto.photo);
} }
private getBodyScrollTop() { private getBodyScrollTop() {

View File

@ -2,7 +2,6 @@
import {Component, Input, ElementRef, ViewChild} from "@angular/core"; import {Component, Input, ElementRef, ViewChild} from "@angular/core";
import {Photo} from "../../../../common/entities/Photo"; import {Photo} from "../../../../common/entities/Photo";
import {Directory} from "../../../../common/entities/Directory";
import {IRenderable, Dimension} from "../../model/IRenderable"; import {IRenderable, Dimension} from "../../model/IRenderable";
@Component({ @Component({
@ -11,15 +10,14 @@ import {IRenderable, Dimension} from "../../model/IRenderable";
styleUrls: ['app/gallery/photo/photo.gallery.component.css'], styleUrls: ['app/gallery/photo/photo.gallery.component.css'],
}) })
export class GalleryPhotoComponent implements IRenderable { export class GalleryPhotoComponent implements IRenderable {
@Input() photo:Photo; @Input() photo:Photo;
@Input() directory:Directory;
@ViewChild("image") imageRef:ElementRef; @ViewChild("image") imageRef:ElementRef;
constructor() { constructor() {
} }
getPhotoPath() { getPhotoPath() {
return Photo.getThumbnailPath(this.directory, this.photo); return Photo.getThumbnailPath(this.photo);
} }

View File

@ -1,8 +1,9 @@
<div class="col-sm-4 col-md-5 pull-right"> <div class="col-sm-4 col-md-5 pull-right">
<form class="navbar-form" role="search"> <form class="navbar-form" role="search" #SearchForm="ngForm">
<div class="input-group"> <div class="input-group">
<input type="text" class="form-control" placeholder="Search" (keyup)="getSuggestions($event)" (blur)="onFocusLost($event)" <input type="text" class="form-control" placeholder="Search" (keyup)="onSearchChange($event)"
name="srch-term" id="srch-term" autocomplete="off" > (blur)="onFocusLost($event)" [(ngModel)]="searchText" #name="ngForm" ngControl="search"
name="srch-term" id="srch-term" autocomplete="off" >
<div class="autocomplete-list" *ngIf="autoCompleteItems.length > 0" > <div class="autocomplete-list" *ngIf="autoCompleteItems.length > 0" >
<div class="autocomplete-item" *ngFor="let item of autoCompleteItems"> <div class="autocomplete-item" *ngFor="let item of autoCompleteItems">
@ -11,7 +12,8 @@
</div> </div>
<div class="input-group-btn" style="display: block"> <div class="input-group-btn" style="display: block">
<button class="btn btn-default dropdown-toggle" type="button"><i class="glyphicon glyphicon-search"></i> <button class="btn btn-default dropdown-toggle" type="button" (click)="onSearch()"><i
class="glyphicon glyphicon-search"></i>
</button> </button>
</div> </div>
</div> </div>

View File

@ -4,21 +4,25 @@ import {Component} from "@angular/core";
import {AutoCompleteService} from "./autocomplete.service"; import {AutoCompleteService} from "./autocomplete.service";
import {AutoCompleteItem} from "../../../../common/entities/AutoCompleteItem"; import {AutoCompleteItem} from "../../../../common/entities/AutoCompleteItem";
import {Message} from "../../../../common/entities/Message"; import {Message} from "../../../../common/entities/Message";
import {GalleryService} from "../gallery.service";
import {FORM_DIRECTIVES} from "@angular/common";
@Component({ @Component({
selector: 'gallery-search', selector: 'gallery-search',
templateUrl: 'app/gallery/search/search.gallery.component.html', templateUrl: 'app/gallery/search/search.gallery.component.html',
styleUrls: ['app/gallery/search/search.gallery.component.css'], styleUrls: ['app/gallery/search/search.gallery.component.css'],
providers: [AutoCompleteService] providers: [AutoCompleteService],
directives: [FORM_DIRECTIVES]
}) })
export class GallerySearchComponent { export class GallerySearchComponent {
autoCompleteItems:Array<AutoCompleteRenderItem> = []; autoCompleteItems:Array<AutoCompleteRenderItem> = [];
private searchText:string = "";
constructor(private _autoCompleteService:AutoCompleteService) { constructor(private _autoCompleteService:AutoCompleteService, private _galleryService:GalleryService) {
} }
getSuggestions(event:KeyboardEvent) { onSearchChange(event:KeyboardEvent) {
let searchText = (<HTMLInputElement>event.target).value; let searchText = (<HTMLInputElement>event.target).value;
if (searchText.trim().length > 0) { if (searchText.trim().length > 0) {
this._autoCompleteService.autoComplete(searchText).then((message:Message<Array<AutoCompleteItem>>) => { this._autoCompleteService.autoComplete(searchText).then((message:Message<Array<AutoCompleteItem>>) => {
@ -32,8 +36,14 @@ export class GallerySearchComponent {
} else { } else {
this.emptyAutoComplete(); this.emptyAutoComplete();
} }
this._galleryService.instantSearch(searchText);
} }
public onSearch() {
this._galleryService.search(this.searchText);
}
private showSuggestions(suggestions:Array<AutoCompleteItem>, searchText:string) { private showSuggestions(suggestions:Array<AutoCompleteItem>, searchText:string) {
this.emptyAutoComplete(); this.emptyAutoComplete();
suggestions.forEach((item)=> { suggestions.forEach((item)=> {