1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2024-12-31 02:29:51 +02:00

implementing basic thumbnail loading

This commit is contained in:
Braun Patrik 2016-06-17 00:05:31 +02:00
parent c5e3d09302
commit 08d6dc994e
20 changed files with 223 additions and 454 deletions

View File

@ -10,7 +10,7 @@ Config.Server = {
port: 80,
imagesFolder: "/demo/images",
thumbnailFolder: "/demo/TEMP",
databaseType: DatabaseType.mongoDB
databaseType: DatabaseType.memory
};
ConfigLoader.init(Config, path.join(__dirname, './../../config.json'), [["PORT", "Server-port"]]);

View File

@ -11,19 +11,11 @@ export class ObjectManagerRepository {
private _searchManager:ISearchManager;
private static _instance:ObjectManagerRepository = null;
public static InitMongoManagers() {
let MongoGalleryManager = require("./mongoose/MongoGalleryManager");
let MongoUserManager = require("./mongoose/MongoUserManager");
let MongoSearchManager = require("./mongoose/MongoSearchManager");
ObjectManagerRepository.getInstance().setGalleryManager(new MongoGalleryManager());
ObjectManagerRepository.getInstance().setUserManager(new MongoUserManager());
ObjectManagerRepository.getInstance().setSearchManager(new MongoSearchManager());
}
public static MemoryMongoManagers() {
let GalleryManager = require("./memory/GalleryManager");
let UserManager = require("./memory/UserManager");
let SearchManager = require("./memory/SearchManager");
let GalleryManager = require("./memory/GalleryManager").GalleryManager;
let UserManager = require("./memory/UserManager").UserManager;
let SearchManager = require("./memory/SearchManager").SearchManager;
ObjectManagerRepository.getInstance().setGalleryManager(new GalleryManager());
ObjectManagerRepository.getInstance().setUserManager(new UserManager());
ObjectManagerRepository.getInstance().setSearchManager(new SearchManager());

View File

@ -1,55 +0,0 @@
///<reference path="../../../typings/index.d.ts"/>
import * as mongoose from "mongoose";
import {Schema} from "mongoose";
export class DatabaseManager {
private static _instance:DatabaseManager = null;
private connectionError = false;
private errorObject = null;
private connectionOpen = false;
constructor() {
mongoose.connect('mongodb://localhost/EQZT6L');
}
public static getInstance(onError?:(err)=>void, onConnected?:() =>void) {
if (DatabaseManager._instance === null) {
DatabaseManager._instance = new DatabaseManager();
}
return DatabaseManager._instance;
}
public onConnectionError(onError:(err) => void){
if (this.connectionError === true) {
return onError(DatabaseManager._instance.errorObject);
}
mongoose.connection.once('error', (err) => {
this.connectionError = true;
this.errorObject = err;
onError(err);
});
}
public onConnected(onConnected:() => void){
if (this.connectionOpen === true) {
return onConnected();
}
mongoose.connection.once('open', (err) => {
this.connectionOpen = true;
onConnected();
});
}
public getModel(name:string, schema:any) {
return mongoose.model(name, new Schema(schema));
}
public disconnect() {
mongoose.disconnect();
}
public isConnectionError() {
return this.connectionError;
}
}

View File

@ -1,80 +0,0 @@
import * as path from "path";
import {Directory} from "../../../common/entities/Directory";
import {IGalleryManager} from "../IGalleryManager";
import {DiskManager} from "../DiskManger";
import {Utils} from "../../../common/Utils";
import {DirectoryModel} from "./entities/DirectoryModel";
import {PhotoModel} from "./entities/PhotoModel";
import {Photo} from "../../../common/entities/Photo";
export class MongoGalleryManager implements IGalleryManager {
constructor() {
}
public listDirectory(relativeDirectoryName, cb:(error:any, result:Directory) => void) {
let directoryName = path.basename(relativeDirectoryName);
let directoryParent = path.join(path.dirname(relativeDirectoryName), "/");
DirectoryModel.findOne({
name: directoryName,
path: directoryParent
}).populate('photos').populate('directories').exec((err, res:any) => {
if (err || !res) {
return this.indexDirectory(relativeDirectoryName, cb);
}
return cb(err, this.modelToEntity(res));
});
}
public indexDirectory(relativeDirectoryName, cb:(error:any, result:Directory) => void) {
DiskManager.scanDirectory(relativeDirectoryName, (err, scannedDirectory)=> {
let arr = [];
scannedDirectory.directories.forEach((value) => {
let dir = new DirectoryModel(value);
Utils.setKeys(dir, value);
dir.save();
arr.push(dir);
});
scannedDirectory.directories = arr;
arr = [];
scannedDirectory.photos.forEach((value) => {
let p = new PhotoModel(value);
Utils.setKeys(p, value);
p.save();
arr.push(p);
});
scannedDirectory.photos = arr;
DirectoryModel.create(scannedDirectory, (err, savedDir)=> {
scannedDirectory.photos.forEach((value:any) => {
value['directory'] = savedDir;
value.save();
});
return cb(err, this.modelToEntity(savedDir));
});
});
}
private modelToEntity(directoryModel:any):Directory {
let directoryEntity = new Directory(directoryModel._id, directoryModel.name, directoryModel.path, directoryModel.lastupdate, [], []);
directoryModel.photos.forEach((photo) => {
let photoEntity = new Photo(photo._id, photo.name, directoryEntity, photo.metadata);
directoryEntity.photos.push(photoEntity);
});
directoryModel.directories.forEach((dir) => {
let dirEntity = new Directory(dir._id, dir.name, dir.path, dir.lastupdate, [], []);
directoryEntity.directories.push(dirEntity);
});
return directoryEntity;
}
}

View File

@ -1,182 +0,0 @@
import {AutoCompleteItem, SearchTypes} from "../../../common/entities/AutoCompleteItem";
import {ISearchManager} from "../ISearchManager";
import {DirectoryModel} from "./entities/DirectoryModel";
import {PhotoModel} from "./entities/PhotoModel";
import {SearchResult} from "../../../common/entities/SearchResult";
export class MongoSearchManager implements ISearchManager {
constructor() {
}
autocomplete(text, cb:(error:any, result:Array<AutoCompleteItem>) => void) {
console.log("autocomplete: " + text);
let items:Array<AutoCompleteItem> = [];
let promises = [];
promises.push(
PhotoModel.find({name: {$regex: text, $options: "i"}})
.limit(10).select('name').exec().then((res:Array<any>)=> {
items = items.concat(this.encapsulateAutoComplete(res.map(r => r.name), SearchTypes.image));
}));
promises.push(
PhotoModel.find({"metadata.positionData.city": {$regex: text, $options: "i"}})
.limit(10).select('metadata.positionData.city').exec().then((res:Array<any>)=> {
items = items.concat(this.encapsulateAutoComplete(res.map(r => r.metadata.positionData.city), SearchTypes.position));
}));
promises.push(
PhotoModel.find({"metadata.positionData.state": {$regex: text, $options: "i"}})
.limit(10).select('metadata.positionData.state').exec().then((res:Array<any>)=> {
items = items.concat(this.encapsulateAutoComplete(res.map(r => r.metadata.positionData.state), SearchTypes.position));
}));
promises.push(
PhotoModel.find({"metadata.positionData.country": {$regex: text, $options: "i"}})
.limit(10).select('metadata.positionData.country').exec().then((res:Array<any>)=> {
items = items.concat(this.encapsulateAutoComplete(res.map(r => r.metadata.positionData.country), SearchTypes.position));
}));
//TODO: fix caseinsensitivity
promises.push(
PhotoModel.find({"metadata.keywords": {$regex: text, $options: "i"}})
.limit(10).select('metadata.keywords').exec().then((res:Array<any>)=> {
res.forEach((photo)=> {
items = items.concat(this.encapsulateAutoComplete(photo.metadata.keywords.filter(k => k.indexOf(text) != -1), SearchTypes.keyword));
});
}));
promises.push(
DirectoryModel.find({
name: {$regex: text, $options: "i"}
}).limit(10).select('name').exec().then((res:Array<any>)=> {
items = items.concat(this.encapsulateAutoComplete(res.map(r => r.name), SearchTypes.directory));
}));
Promise.all(promises).then(()=> {
return cb(null, this.autoCompleteItemsUnique(items));
}).catch((err)=> {
console.error(err);
return cb(err, null);
});
}
search(text:string, searchType:SearchTypes, cb:(error:any, result:SearchResult) => void) {
console.log("search: " + text + ", type:" + searchType);
let result:SearchResult = new SearchResult();
let promises = [];
let photoFilterOR = [];
result.searchText = text;
result.searchType = searchType;
if (!searchType || searchType === SearchTypes.image) {
photoFilterOR.push({name: {$regex: text, $options: "i"}});
}
if (!searchType || searchType === SearchTypes.position) {
photoFilterOR.push({"metadata.positionData.city": {$regex: text, $options: "i"}});
photoFilterOR.push({"metadata.positionData.state": {$regex: text, $options: "i"}});
photoFilterOR.push({"metadata.positionData.country": {$regex: text, $options: "i"}});
}
if (!searchType || searchType === SearchTypes.keyword) {
photoFilterOR.push({"metadata.keywords": {$regex: text, $options: "i"}});
}
let photoFilter = {};
if (photoFilterOR.length == 1) {
photoFilter = photoFilterOR[0];
} else {
photoFilter = {$or: photoFilterOR};
}
if (!searchType || photoFilterOR.length > 0) {
promises.push(PhotoModel.find(photoFilter).populate('directory', 'name path').exec().then((res:Array<any>) => {
result.photos = res;
}));
}
if (!searchType || searchType === SearchTypes.directory) {
promises.push(DirectoryModel.find({
name: {
$regex: text,
$options: "i"
}
}).exec().then((res:Array<any>) => {
result.directories = res;
}));
}
Promise.all(promises).then(()=> {
return cb(null, result);
}).catch((err)=> {
console.error(err);
return cb(err, null);
});
}
instantSearch(text, cb:(error:any, result:SearchResult) => void) {
console.log("instantSearch: " + text);
let result:SearchResult = new SearchResult();
result.searchText = text;
PhotoModel.find({
$or: [
{name: {$regex: text, $options: "i"}},
{"metadata.positionData.city": {$regex: text, $options: "i"}},
{"metadata.positionData.state": {$regex: text, $options: "i"}},
{"metadata.positionData.country": {$regex: text, $options: "i"}},
{"metadata.keywords": {$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:SearchTypes) {
let res = [];
values.forEach((value)=> {
res.push(new AutoCompleteItem(value, type));
});
return res;
}
private autoCompleteItemsUnique(array:Array<AutoCompleteItem>) {
var a = array.concat();
for (var i = 0; i < a.length; ++i) {
for (var j = i + 1; j < a.length; ++j) {
if (a[i].equals(a[j]))
a.splice(j--, 1);
}
}
return a;
}
}

View File

@ -1,45 +0,0 @@
import {User, UserRoles} from "../../../common/entities/User";
import {IUserManager} from "../IUserManager";
import {UserModel} from "./entities/UserModel";
export class MongoUserManager implements IUserManager {
constructor() {
}
public findOne(filter, cb:(error:any, result:User) => void) {
return UserModel.findOne(filter, function (err, result:any) {
return cb(err, result);
});
}
public find(filter, cb:(error:any, result:Array<User>) => void) {
UserModel.find(filter, function (err, result:Array<any>) {
return cb(err, result);
});
}
public createUser(user, cb:(error:any, result:User) => void) {
UserModel.create(user, cb);
}
public deleteUser(id:number, cb:(error:any) => void) {
UserModel.remove({id: id}, cb);
}
public changeRole(id:number, newRole:UserRoles, cb:(error:any, result:string) => void) {
return UserModel.update({id: id}, {role: newRole}, function (err) {
if (!err) {
return cb(err, "ok")
}
return cb(err, null);
});
}
public changePassword(request:any, cb:(error:any, result:string) => void) {
throw new Error("not implemented"); //TODO: implement
}
}

View File

@ -1,16 +0,0 @@
import {DatabaseManager} from "../DatabaseManager";
import {Schema} from "mongoose";
export var DirectoryModel = DatabaseManager.getInstance().getModel('directory', {
name: String,
path: String,
lastUpdate: Date,
directories: [{
type: Schema.Types.ObjectId,
ref: 'directory'
}],
photos: [{
type: Schema.Types.ObjectId,
ref: 'photo'
}]
});

View File

@ -1,39 +0,0 @@
import {DatabaseManager} from "../DatabaseManager";
import {Schema} from "mongoose";
export var PhotoModel = DatabaseManager.getInstance().getModel('photo', {
name: String,
directory: {
type: Schema.Types.ObjectId,
ref: 'directory'
},
metadata: {
keywords: [String],
cameraData: {
ISO: Number,
maker: String,
fStop: Number,
exposure: Number,
focalLength: Number,
lens: String
},
positionData: {
GPSData: {
latitude: Number,
longitude: Number,
altitude: Number
},
country: String,
state: String,
city: String
},
size: {
width: Number,
height: Number
},
creationDate: Date
}
});

View File

@ -1,6 +0,0 @@
import {DatabaseManager} from "../DatabaseManager";
export var UserModel = DatabaseManager.getInstance().getModel('user', {
name: {type: String, index: {unique: true}},
password: String,
role: Number
});

View File

@ -13,7 +13,6 @@ import {ErrorRouter} from "./routes/ErrorRouter";
import {SharingRouter} from "./routes/SharingRouter";
import {DatabaseType} from "./../common/config/Config";
import {ObjectManagerRepository} from "./model/ObjectManagerRepository";
import {DatabaseManager} from "./model/mongoose/DatabaseManager";
import {Config} from "./config/Config";
@ -61,13 +60,7 @@ export class Server {
if (Config.Server.databaseType === DatabaseType.memory) {
ObjectManagerRepository.MemoryMongoManagers();
} else {
ObjectManagerRepository.InitMongoManagers();
DatabaseManager.getInstance().onConnectionError(
()=> {
console.error("MongoDB connection error. Falling back to memory Object Managers");
ObjectManagerRepository.MemoryMongoManagers();
Config.setDatabaseType(DatabaseType.memory);
});
throw new Error("not implemented alternative mangers");
}
new PublicRouter(this.app);

View File

@ -15,6 +15,7 @@ export class Utils {
url += part + "/";
}
url = url.replace("//", "/");
return url.substring(0, url.length - 1);
}

View File

@ -1,5 +1,5 @@
export enum DatabaseType{
memory, mongoDB
memory
}
interface ServerConfig {

View File

@ -4,7 +4,8 @@ export class Photo {
constructor(public id?:number,
public name?:string,
public directory?:Directory,
public metadata?:PhotoMetadata) {
public metadata?:PhotoMetadata,
public readyThumbnails:Array<number> = []) {
}
}

View File

@ -11,6 +11,7 @@ import {UserService} from "./model/network/user.service.ts";
import {GalleryService} from "./gallery/gallery.service";
import {AdminComponent} from "./admin/admin.component";
import {NetworkService} from "./model/network/network.service";
import {ThumbnailLoaderService} from "./gallery/grid/thumnailLoader.service";
@Component({
@ -23,7 +24,8 @@ import {NetworkService} from "./model/network/network.service";
NetworkService,
UserService,
GalleryService,
AuthenticationService]
AuthenticationService,
ThumbnailLoaderService]
})
@RouteConfig([
{

View File

@ -6,9 +6,24 @@ export class GridPhoto {
}
getThumbnailPath() {
thumbnailLoaded() {
if (!this.isThumbnailAvailable()) {
this.photo.readyThumbnails.push(this.getThumbnailSize());
}
}
getThumbnailSize() {
let renderSize = Math.sqrt(this.renderWidth * this.renderHeight);
let size = Utils.findClosest(renderSize, Config.Client.thumbnailSizes);
return Utils.findClosest(renderSize, Config.Client.thumbnailSizes);
}
isThumbnailAvailable() {
return this.photo.readyThumbnails.indexOf(this.getThumbnailSize()) != -1;
}
getThumbnailPath() {
let size = this.getThumbnailSize();
return Utils.concatUrls("/api/gallery/content/", this.photo.directory.path, this.photo.directory.name, this.photo.name, "thumbnail", size.toString());
}

View File

@ -45,3 +45,84 @@ a {
width: 100%;
}
.sk-cube-grid {
width: 100%;
height: 100%;
}
.sk-cube-grid .sk-cube {
width: 33%;
height: 33%;
background-color: rgba(0, 0, 0, 0.1);
float: left;
-webkit-animation: sk-cubeGridScaleDelay 4.6s infinite ease-in-out;
animation: sk-cubeGridScaleDelay 4.6s infinite ease-in-out;
}
.sk-cube-grid .sk-cube1 {
-webkit-animation-delay: 2.4s;
animation-delay: 2.4s;
}
.sk-cube-grid .sk-cube2 {
-webkit-animation-delay: 2.6s;
animation-delay: 2.6s;
}
.sk-cube-grid .sk-cube3 {
-webkit-animation-delay: 2.8s;
animation-delay: 2.8s;
}
.sk-cube-grid .sk-cube4 {
-webkit-animation-delay: 2.2s;
animation-delay: 2.2s;
}
.sk-cube-grid .sk-cube5 {
-webkit-animation-delay: 2.4s;
animation-delay: 2.4s;
}
.sk-cube-grid .sk-cube6 {
-webkit-animation-delay: 2.6s;
animation-delay: 2.6s;
}
.sk-cube-grid .sk-cube7 {
-webkit-animation-delay: 2s;
animation-delay: 2s;
}
.sk-cube-grid .sk-cube8 {
-webkit-animation-delay: 2.2s;
animation-delay: 2.2s;
}
.sk-cube-grid .sk-cube9 {
-webkit-animation-delay: 2.4s;
animation-delay: 2.4s;
}
@-webkit-keyframes sk-cubeGridScaleDelay {
0%, 70%, 100% {
-webkit-transform: scale3D(1, 1, 1);
transform: scale3D(1, 1, 1);
}
35% {
-webkit-transform: scale3D(0, 0, 1);
transform: scale3D(0, 0, 1);
}
}
@keyframes sk-cubeGridScaleDelay {
0%, 70%, 100% {
-webkit-transform: scale3D(1, 1, 1);
transform: scale3D(1, 1, 1);
}
35% {
-webkit-transform: scale3D(0, 0, 1);
transform: scale3D(0, 0, 1);
}
}

View File

@ -1,6 +1,20 @@
<div class="photo-container" (mouseover)="hover()" (mouseout)="mouseOut()">
<img #image [src]="gridPhoto.getThumbnailPath()">
<div class="info" #info [style.margin-top.px]="-infoStyle.height" [style.background]="infoStyle.background">
<img #image [src]="imageSrc" [hidden]="!showImage">
<div class="sk-cube-grid" *ngIf="!showImage">
<div class="sk-cube sk-cube1"></div>
<div class="sk-cube sk-cube2"></div>
<div class="sk-cube sk-cube3"></div>
<div class="sk-cube sk-cube4"></div>
<div class="sk-cube sk-cube5"></div>
<div class="sk-cube sk-cube6"></div>
<div class="sk-cube sk-cube7"></div>
<div class="sk-cube sk-cube8"></div>
<div class="sk-cube sk-cube9"></div>
</div>
<!--Info box -->
<div #info [hidden]="!showImage" class="info" [style.margin-top.px]="-infoStyle.height"
[style.background]="infoStyle.background">
<div class="photo-name">{{gridPhoto.photo.name}}</div>
<div class="photo-position" *ngIf="gridPhoto.photo.metadata.positionData">

View File

@ -1,11 +1,12 @@
///<reference path="../../../../browser.d.ts"/>
import {Component, Input, ElementRef, ViewChild} from "@angular/core";
import {Component, Input, ElementRef, ViewChild, OnChanges} from "@angular/core";
import {IRenderable, Dimension} from "../../../model/IRenderable";
import {GridPhoto} from "../GridPhoto";
import {SearchTypes} from "../../../../../common/entities/AutoCompleteItem";
import {RouterLink} from "@angular/router-deprecated";
import {Config} from "../../../config/Config";
import {ThumbnailLoaderService} from "../thumnailLoader.service";
@Component({
selector: 'gallery-grid-photo',
@ -13,23 +14,42 @@ import {Config} from "../../../config/Config";
styleUrls: ['app/gallery/grid/photo/photo.grid.gallery.component.css'],
directives: [RouterLink],
})
export class GalleryPhotoComponent implements IRenderable {
export class GalleryPhotoComponent implements IRenderable, OnChanges {
@Input() gridPhoto:GridPhoto;
@ViewChild("image") imageRef:ElementRef;
@ViewChild("info") infoDiv:ElementRef;
imageSrc = "#";
showImage = false;
infoStyle = {
height: 0,
background: ""
};
SearchTypes:any = [];
searchEnabled:boolean = true;
constructor() {
constructor(private thumbnailService:ThumbnailLoaderService) {
this.SearchTypes = SearchTypes;
this.searchEnabled = Config.Client.Search.searchEnabled;
}
ngOnChanges() {
if (this.gridPhoto.isThumbnailAvailable()) {
this.imageSrc = this.gridPhoto.getThumbnailPath();
// this.showImage = true;
} else {
this.thumbnailService.loadImage(this.gridPhoto).then(()=> {
this.imageSrc = this.gridPhoto.getThumbnailPath();
// this.showImage = true;
this.gridPhoto.thumbnailLoaded();
}).catch((error)=> {
console.error("something bad happened");
});
}
}
getPositionText():string {
if (!this.gridPhoto) {
return ""

View File

@ -0,0 +1,73 @@
///<reference path="../../../browser.d.ts"/>
import {Injectable} from "@angular/core";
import {GridPhoto} from "./GridPhoto";
@Injectable()
export class ThumbnailLoaderService {
que:Array<ThumbnailTask> = [];
constructor() {
}
loadImage(gridPhoto:GridPhoto):Promise<void> {
console.log("[LOAD IMG]" + gridPhoto.photo.name);
return new Promise<void>((resolve:Function, reject:Function)=> {
let tmp:ThumbnailTask = null;
for (let i = 0; i < this.que.length; i++) {
if (this.que[i].src == gridPhoto.getThumbnailPath()) {
tmp = this.que[i];
break;
}
}
if (tmp != null) {
tmp.resolve.push(resolve);
tmp.reject.push(reject);
} else {
this.que.push({src: gridPhoto.getThumbnailPath(), resolve: [resolve], reject: [reject]});
}
this.run();
});
}
isRunning:boolean = false;
run() {
if (this.que.length === 0 || this.isRunning === true) {
return;
}
this.isRunning = true;
let task = this.que.shift();
console.log("loadingstarted: " + task.src);
let curImg = new Image();
curImg.src = task.src;
curImg.onload = () => {
console.log(task.src + "done");
task.resolve.forEach((resolve:()=>{}) => {
resolve();
});
this.isRunning = false;
this.run();
};
curImg.onerror = (error) => {
console.error(task.src + "error");
task.reject.forEach((reject:(error)=>{}) => {
reject(error);
});
this.isRunning = false;
this.run();
};
}
}
interface ThumbnailTask {
src:string;
resolve:Array<Function>;
reject:Array<Function>;
}

View File

@ -11,7 +11,7 @@ module.exports = {
library: ['peer']
},
// Turn on sourcemaps
//devtool: 'source-map',
devtool: 'source-map',
resolve: {
extensions: ['', '.webpack.js', '.web.js', '.ts', '.js'],