1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-09-16 09:16:27 +02:00

adding angular-cli support (causes major refactoring)

This commit is contained in:
Braun Patrik
2017-06-10 22:32:56 +02:00
parent e7cb6311a9
commit 8b9f287a88
108 changed files with 3820 additions and 3752 deletions

58
.angular-cli.json Normal file
View File

@@ -0,0 +1,58 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"project": {
"name": "pigallery2"
},
"apps": [
{
"root": "frontend",
"outDir": "dist",
"assets": [
"assets",
"favicon.ico",
"config_inject.ejs"
],
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"styles.css"
],
"scripts": [],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
],
"e2e": {
"protractor": {
"config": "./protractor.conf.js"
}
},
"lint": [
{
"project": "src/tsconfig.app.json"
},
{
"project": "src/tsconfig.spec.json"
},
{
"project": "test/e2e/tsconfig.e2e.json"
}
],
"test": {
"karma": {
"config": "./karma.conf.js"
}
},
"defaults": {
"styleExt": "css",
"component": {}
}
}

13
.editorconfig Normal file
View File

@@ -0,0 +1,13 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false

9
.gitignore vendored
View File

@@ -2,10 +2,8 @@
PiGallery2.iml PiGallery2.iml
node_modules/ node_modules/
pigallery2.zip pigallery2.zip
frontend/app/**/*.js frontend/**/*.js
frontend/app/**/*.js.map frontend/**/*.js.map
frontend/main.js
frontend/main.js.map
frontend/dist frontend/dist
backend/**/*.js backend/**/*.js
backend/**/*.js.map backend/**/*.js.map
@@ -14,6 +12,9 @@ common/**/*.js.map
test/coverage test/coverage
test/backend/**/*.js test/backend/**/*.js
test/backend/**/*.js.map test/backend/**/*.js.map
test/e2e/**/*.js
test/e2e/**/*.js.map
demo/TEMP/ demo/TEMP/
config.json config.json
users.db users.db
dist/

View File

@@ -1,33 +1,33 @@
import * as winston from "winston"; import * as winston from "winston";
declare module 'winston' { declare module 'winston' {
interface LoggerInstance { interface LoggerInstance {
logFileName: string; logFileName: string;
logFilePath: string; logFilePath: string;
} }
} }
export const winstonSettings = { export const winstonSettings = {
transports: [ transports: [
new winston.transports.Console({ new winston.transports.Console({
level: 'silly', level: 'silly',
handleExceptions: true, handleExceptions: true,
json: false, json: false,
colorize: true, colorize: true,
timestamp: function () { timestamp: function () {
return (new Date()).toLocaleString(); return (new Date()).toLocaleString();
}, },
label: "innerLabel", label: "innerLabel",
formatter: (options) => { formatter: (options) => {
// Return string will be passed to logger. // Return string will be passed to logger.
return options.timestamp() + '[' + winston['config']['colorize'](options.level, options.level.toUpperCase()) + '] ' + return options.timestamp() + '[' + winston['config']['colorize'](options.level, options.level.toUpperCase()) + '] ' +
(undefined !== options.message ? options.message : '') + (undefined !== options.message ? options.message : '') +
(options.meta && Object.keys(options.meta).length ? '\n\t' + JSON.stringify(options.meta) : '' ); (options.meta && Object.keys(options.meta).length ? '\n\t' + JSON.stringify(options.meta) : '' );
}, },
debugStdout: true debugStdout: true
}) })
], ],
exitOnError: false exitOnError: false
}; };
export const Logger = new winston.Logger(winstonSettings); export const Logger = new winston.Logger(winstonSettings);

View File

@@ -2,23 +2,23 @@ import * as path from "path";
import {Config} from "../common/config/private/Config"; import {Config} from "../common/config/private/Config";
class ProjectPathClass { class ProjectPathClass {
public Root: string; public Root: string;
public ImageFolder: string; public ImageFolder: string;
public ThumbnailFolder: string; public ThumbnailFolder: string;
isAbsolutePath(pathStr: string) { isAbsolutePath(pathStr: string) {
return path.resolve(pathStr) === path.normalize(pathStr); return path.resolve(pathStr) === path.normalize(pathStr);
} }
normalizeRelative(pathStr: string) { normalizeRelative(pathStr: string) {
return path.join(pathStr, path.sep); return path.join(pathStr, path.sep);
} }
constructor() { constructor() {
this.Root = path.join(__dirname, "/../"); this.Root = path.join(__dirname, "/../");
this.ImageFolder = this.isAbsolutePath(Config.Server.imagesFolder) ? Config.Server.imagesFolder : path.join(this.Root, Config.Server.imagesFolder); this.ImageFolder = this.isAbsolutePath(Config.Server.imagesFolder) ? Config.Server.imagesFolder : path.join(this.Root, Config.Server.imagesFolder);
this.ThumbnailFolder = this.isAbsolutePath(Config.Server.thumbnail.folder) ? Config.Server.thumbnail.folder : path.join(this.Root, Config.Server.thumbnail.folder); this.ThumbnailFolder = this.isAbsolutePath(Config.Server.thumbnail.folder) ? Config.Server.thumbnail.folder : path.join(this.Root, Config.Server.thumbnail.folder);
} }
} }
export let ProjectPath = new ProjectPathClass(); export const ProjectPath = new ProjectPathClass();

View File

@@ -17,133 +17,133 @@ const LOG_TAG = "[GalleryMWs]";
export class GalleryMWs { export class GalleryMWs {
public static listDirectory(req: Request, res: Response, next: NextFunction) { public static listDirectory(req: Request, res: Response, next: NextFunction) {
let directoryName = req.params.directory || "/"; let directoryName = req.params.directory || "/";
let absoluteDirectoryName = path.join(ProjectPath.ImageFolder, directoryName); let absoluteDirectoryName = path.join(ProjectPath.ImageFolder, directoryName);
if (!fs.statSync(absoluteDirectoryName).isDirectory()) { if (!fs.statSync(absoluteDirectoryName).isDirectory()) {
return next(); return next();
} }
ObjectManagerRepository.getInstance().getGalleryManager().listDirectory(directoryName, (err, directory: DirectoryDTO) => { ObjectManagerRepository.getInstance().getGalleryManager().listDirectory(directoryName, (err, directory: DirectoryDTO) => {
if (err || !directory) { if (err || !directory) {
Logger.warn(LOG_TAG, "Error during listing the directory", err); Logger.warn(LOG_TAG, "Error during listing the directory", err);
console.error(err); console.error(err);
return next(new Error(ErrorCodes.GENERAL_ERROR, err)); return next(new Error(ErrorCodes.GENERAL_ERROR, err));
} }
req.resultPipe = new ContentWrapper(directory, null); req.resultPipe = new ContentWrapper(directory, null);
return next(); return next();
}); });
}
public static removeCyclicDirectoryReferences(req: Request, res: Response, next: NextFunction) {
if (!req.resultPipe)
return next();
let cw: ContentWrapper = req.resultPipe;
let removeDirs = (dir) => {
dir.photos.forEach((photo: PhotoDTO) => {
photo.directory = null;
});
dir.directories.forEach((directory: DirectoryDTO) => {
removeDirs(directory);
directory.parent = null;
});
};
if (cw.directory) {
removeDirs(cw.directory);
} }
public static removeCyclicDirectoryReferences(req: Request, res: Response, next: NextFunction) { return next();
if (!req.resultPipe) }
return next();
let cw: ContentWrapper = req.resultPipe;
let removeDirs = (dir) => {
dir.photos.forEach((photo: PhotoDTO) => {
photo.directory = null;
});
dir.directories.forEach((directory: DirectoryDTO) => {
removeDirs(directory);
directory.parent = null;
});
};
if (cw.directory) {
removeDirs(cw.directory);
}
return next(); public static loadImage(req: Request, res: Response, next: NextFunction) {
if (!(req.params.imagePath)) {
return next();
}
let fullImagePath = path.join(ProjectPath.ImageFolder, req.params.imagePath);
if (fs.statSync(fullImagePath).isDirectory()) {
return next();
}
//check if thumbnail already exist
if (fs.existsSync(fullImagePath) === false) {
return next(new Error(ErrorCodes.GENERAL_ERROR, "no such file :" + fullImagePath));
}
req.resultPipe = fullImagePath;
return next();
}
public static search(req: Request, res: Response, next: NextFunction) {
if (Config.Client.Search.searchEnabled === false) {
return next();
}
if (!(req.params.text)) {
return next();
}
let type: SearchTypes;
if (req.query.type) {
type = parseInt(req.query.type);
}
ObjectManagerRepository.getInstance().getSearchManager().search(req.params.text, type, (err, result: SearchResultDTO) => {
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) {
if (Config.Client.Search.instantSearchEnabled === false) {
return next();
}
if (!(req.params.text)) {
return next();
} }
public static loadImage(req: Request, res: Response, next: NextFunction) { ObjectManagerRepository.getInstance().getSearchManager().instantSearch(req.params.text, (err, result: SearchResultDTO) => {
if (!(req.params.imagePath)) { if (err || !result) {
return next(); return next(new Error(ErrorCodes.GENERAL_ERROR, err));
} }
req.resultPipe = new ContentWrapper(null, result);
return next();
});
}
let fullImagePath = path.join(ProjectPath.ImageFolder, req.params.imagePath); public static autocomplete(req: Request, res: Response, next: NextFunction) {
if (fs.statSync(fullImagePath).isDirectory()) { if (Config.Client.Search.autocompleteEnabled === false) {
return next(); return next();
} }
if (!(req.params.text)) {
//check if thumbnail already exist return next();
if (fs.existsSync(fullImagePath) === false) {
return next(new Error(ErrorCodes.GENERAL_ERROR, "no such file :" + fullImagePath));
}
req.resultPipe = fullImagePath;
return next();
} }
ObjectManagerRepository.getInstance().getSearchManager().autocomplete(req.params.text, (err, items: Array<AutoCompleteItem>) => {
public static search(req: Request, res: Response, next: NextFunction) { if (err || !items) {
if (Config.Client.Search.searchEnabled === false) { return next(new Error(ErrorCodes.GENERAL_ERROR, err));
return next(); }
} req.resultPipe = items;
return next();
if (!(req.params.text)) { });
return next(); }
}
let type: SearchTypes;
if (req.query.type) {
type = parseInt(req.query.type);
}
ObjectManagerRepository.getInstance().getSearchManager().search(req.params.text, type, (err, result: SearchResultDTO) => {
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) { }
if (Config.Client.Search.instantSearchEnabled === false) {
return next();
}
if (!(req.params.text)) {
return next();
}
ObjectManagerRepository.getInstance().getSearchManager().instantSearch(req.params.text, (err, result: SearchResultDTO) => {
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) {
if (Config.Client.Search.autocompleteEnabled === false) {
return next();
}
if (!(req.params.text)) {
return next();
}
ObjectManagerRepository.getInstance().getSearchManager().autocomplete(req.params.text, (err, items: Array<AutoCompleteItem>) => {
if (err || !items) {
return next(new Error(ErrorCodes.GENERAL_ERROR, err));
}
req.resultPipe = items;
return next();
});
}
}

View File

@@ -5,49 +5,49 @@ import {Message} from "../../common/entities/Message";
export class RenderingMWs { export class RenderingMWs {
public static renderResult(req:Request, res:Response, next:NextFunction) { public static renderResult(req: Request, res: Response, next: NextFunction) {
if (!req.resultPipe) if (!req.resultPipe)
return next(); return next();
return RenderingMWs.renderMessage(res, req.resultPipe); return RenderingMWs.renderMessage(res, req.resultPipe);
}
public static renderSessionUser(req: Request, res: Response, next: NextFunction) {
if (!(req.session.user)) {
return next(new Error(ErrorCodes.GENERAL_ERROR));
} }
let user = Utils.clone(req.session.user);
delete user.password;
RenderingMWs.renderMessage(res, user);
}
public static renderSessionUser(req:Request, res:Response, next:NextFunction) { public static renderFile(req: Request, res: Response, next: NextFunction) {
if (!(req.session.user)) { if (!req.resultPipe)
return next(new Error(ErrorCodes.GENERAL_ERROR)); return next();
}
let user = Utils.clone(req.session.user); return res.sendFile(req.resultPipe);
delete user.password; }
RenderingMWs.renderMessage(res, user);
} public static renderOK(req: Request, res: Response, next: NextFunction) {
let message = new Message<string>(null, "ok");
public static renderFile(req:Request, res:Response, next:NextFunction) { res.json(message);
if (!req.resultPipe) }
return next();
public static renderError(err: any, req: Request, res: Response, next: NextFunction): any {
return res.sendFile(req.resultPipe); if (err instanceof Error) {
} let message = new Message<any>(err, null);
return res.json(message);
public static renderOK(req:Request, res:Response, next:NextFunction) {
let message = new Message<string>(null, "ok");
res.json(message);
}
public static renderError(err:any, req:Request, res:Response, next:NextFunction):any {
if (err instanceof Error) {
let message = new Message<any>(err, null);
return res.json(message);
}
return next(err);
} }
return next(err);
}
protected static renderMessage<T>(res:Response, content:T) { protected static renderMessage<T>(res: Response, content: T) {
let message = new Message<T>(null, content); let message = new Message<T>(null, content);
res.json(message); res.json(message);
} }
} }

View File

@@ -9,7 +9,7 @@ import {ContentWrapper} from "../../../common/entities/ConentWrapper";
import {DirectoryDTO} from "../../../common/entities/DirectoryDTO"; import {DirectoryDTO} from "../../../common/entities/DirectoryDTO";
import {ProjectPath} from "../../ProjectPath"; import {ProjectPath} from "../../ProjectPath";
import {PhotoDTO} from "../../../common/entities/PhotoDTO"; import {PhotoDTO} from "../../../common/entities/PhotoDTO";
import {hardwareRenderer, softwareRenderer} from "./THRenderers"; import {hardwareRenderer, RendererInput, softwareRenderer} from "./THRenderers";
import {Config} from "../../../common/config/private/Config"; import {Config} from "../../../common/config/private/Config";
@@ -139,20 +139,35 @@ export class ThumbnailGeneratorMWs {
this.initPools(); this.initPools();
//run on other thread //run on other thread
pool.send({
let input = <RendererInput>{
imagePath: imagePath, imagePath: imagePath,
size: size, size: size,
thPath: thPath, thPath: thPath,
makeSquare: makeSquare, makeSquare: makeSquare,
qualityPriority: Config.Server.thumbnail.qualityPriority, qualityPriority: Config.Server.thumbnail.qualityPriority,
__dirname: __dirname, __dirname: __dirname,
}) };
.on('done', (out) => { if (Config.Server.enableThreading == true) {
return next(out); pool.send(imagePath)
}).on('error', (error) => { .on('done', (out) => {
console.log(error); return next(out);
return next(new Error(ErrorCodes.THUMBNAIL_GENERATION_ERROR, error)); }).on('error', (error) => {
}); console.log(error);
return next(new Error(ErrorCodes.THUMBNAIL_GENERATION_ERROR, error));
});
} else {
try {
if (Config.Server.thumbnail.hardwareAcceleration == true) {
hardwareRenderer(input, out => next(out));
} else {
softwareRenderer(input, out => next(out));
}
}catch (error){
console.log(error);
return next(new Error(ErrorCodes.THUMBNAIL_GENERATION_ERROR, error));
}
}
} }
private static generateThumbnailName(imagePath: string, size: number): string { private static generateThumbnailName(imagePath: string, size: number): string {

View File

@@ -1,228 +1,64 @@
///<reference path="exif.d.ts"/> ///<reference path="exif.d.ts"/>
import * as path from "path"; import * as path from "path";
import {DirectoryDTO} from "../../common/entities/DirectoryDTO"; import {DirectoryDTO} from "../../common/entities/DirectoryDTO";
import {
CameraMetadata,
GPSMetadata,
ImageSize,
PhotoDTO,
PhotoMetadata,
PositionMetaData
} from "../../common/entities/PhotoDTO";
import {ProjectPath} from "../ProjectPath"; import {ProjectPath} from "../ProjectPath";
import {Logger} from "../Logger"; import {Logger} from "../Logger";
import {diskManagerTask, DiskManagerTask} from "./DiskMangerTask";
import {Config} from "../../common/config/private/Config";
const Pool = require('threads').Pool; const Pool = require('threads').Pool;
const pool = new Pool(); const pool = new Pool();
const LOG_TAG = "[DiskManager]"; const LOG_TAG = "[DiskManager]";
interface PoolInput {
relativeDirectoryName: string;
directoryName: string;
directoryParent: string;
absoluteDirectoryName: string;
}
pool.run( pool.run(diskManagerTask);
(input: PoolInput, done) => {
const fs = require("fs");
const path = require("path");
const mime = require("mime");
const iptc = require("node-iptc");
const exif_parser = require("exif-parser");
let isImage = (fullPath: string) => {
let imageMimeTypes = [
'image/bmp',
'image/gif',
'image/jpeg',
'image/png',
'image/pjpeg',
'image/tiff',
'image/webp',
'image/x-tiff',
'image/x-windows-bmp'
];
let extension = mime.lookup(fullPath);
return imageMimeTypes.indexOf(extension) !== -1;
};
let loadPhotoMetadata = (fullPath: string): Promise<PhotoMetadata> => {
return new Promise<PhotoMetadata>((resolve: (metadata: PhotoMetadata) => void, reject) => {
fs.readFile(fullPath, function (err, data) {
if (err) {
return reject({file: fullPath, error: err});
}
try {
const exif = exif_parser.create(data).parse();
const iptcData = iptc(data);
const imageSize: ImageSize = {width: exif.imageSize.width, height: exif.imageSize.height};
const cameraData: CameraMetadata = {
ISO: exif.tags.ISO,
model: exif.tags.Modeol,
maker: exif.tags.Make,
fStop: exif.tags.FNumber,
exposure: exif.tags.ExposureTime,
focalLength: exif.tags.FocalLength,
lens: exif.tags.LensModel,
};
const GPS: GPSMetadata = {
latitude: exif.tags.GPSLatitude,
longitude: exif.tags.GPSLongitude,
altitude: exif.tags.GPSAltitude
};
const positionData: PositionMetaData = {
GPSData: GPS,
country: iptcData.country_or_primary_location_name,
state: iptcData.province_or_state,
city: iptcData.city
};
//Decode characters to UTF8
const decode = (s: any) => {
for (let a, b, i = -1, l = (s = s.split("")).length, o = String.fromCharCode, c = "charCodeAt"; ++i < l;
((a = s[i][c](0)) & 0x80) &&
(s[i] = (a & 0xfc) == 0xc0 && ((b = s[i + 1][c](0)) & 0xc0) == 0x80 ?
o(((a & 0x03) << 6) + (b & 0x3f)) : o(128), s[++i] = "")
);
return s.join("");
};
const keywords: string[] = (iptcData.keywords || []).map((s: string) => decode(s));
const creationDate: number = iptcData.date_time ? iptcData.date_time.getTime() : 0;
const metadata: PhotoMetadata = <PhotoMetadata>{
keywords: keywords,
cameraData: cameraData,
positionData: positionData,
size: imageSize,
creationDate: creationDate
};
return resolve(metadata);
} catch (err) {
return reject({file: fullPath, error: err});
}
});
});
};
let parseDir = (directoryInfo: {
relativeDirectoryName: string,
directoryName: string,
directoryParent: string,
absoluteDirectoryName: string
}, maxPhotos: number = null, photosOnly: boolean = false): Promise<DirectoryDTO> => {
return new Promise<DirectoryDTO>((resolve, reject) => {
let promises: Array<Promise<any>> = [];
let directory = <DirectoryDTO>{
name: directoryInfo.directoryName,
path: directoryInfo.directoryParent,
lastUpdate: Date.now(),
directories: [],
photos: []
};
fs.readdir(directoryInfo.absoluteDirectoryName, (err, list) => {
if (err) {
return reject(err);
}
try {
for (let i = 0; i < list.length; i++) {
let file = list[i];
let fullFilePath = path.normalize(path.resolve(directoryInfo.absoluteDirectoryName, file));
if (photosOnly == false && fs.statSync(fullFilePath).isDirectory()) {
let promise = parseDir({
relativeDirectoryName: path.join(directoryInfo.relativeDirectoryName, path.sep),
directoryName: file,
directoryParent: path.join(directoryInfo.relativeDirectoryName, path.sep),
absoluteDirectoryName: fullFilePath
},
5, true
).then((dir) => {
directory.directories.push(dir);
});
promises.push(promise);
} else if (isImage(fullFilePath)) {
let promise = loadPhotoMetadata(fullFilePath).then((photoMetadata) => {
directory.photos.push(<PhotoDTO>{
name: file,
directory: null,
metadata: photoMetadata
});
});
promises.push(promise);
if (maxPhotos != null && promises.length > maxPhotos) {
break;
}
}
}
Promise.all(promises).then(() => {
return resolve(directory);
}).catch((err) => {
return reject({directoryInfo: directoryInfo, error: err});
});
} catch (err) {
return reject({directoryInfo: directoryInfo, error: err});
}
});
});
};
parseDir(input).then((dir) => {
done(null, dir);
}).catch((err) => {
done(err, null);
});
});
export class DiskManager { export class DiskManager {
public static scanDirectory(relativeDirectoryName: string, cb: (error: any, result: DirectoryDTO) => void) { public static scanDirectory(relativeDirectoryName: string, cb: (error: any, result: DirectoryDTO) => void) {
Logger.silly(LOG_TAG, "scanning directory:", relativeDirectoryName); Logger.silly(LOG_TAG, "scanning directory:", relativeDirectoryName);
let directoryName = path.basename(relativeDirectoryName); let directoryName = path.basename(relativeDirectoryName);
let directoryParent = path.join(path.dirname(relativeDirectoryName), path.sep); let directoryParent = path.join(path.dirname(relativeDirectoryName), path.sep);
let absoluteDirectoryName = path.join(ProjectPath.ImageFolder, relativeDirectoryName); let absoluteDirectoryName = path.join(ProjectPath.ImageFolder, relativeDirectoryName);
pool.send({ let input = <DiskManagerTask.PoolInput>{
relativeDirectoryName, relativeDirectoryName,
directoryName, directoryName,
directoryParent, directoryParent,
absoluteDirectoryName absoluteDirectoryName
}).on('done', (error: any, result: DirectoryDTO) => { };
if (error || !result) {
return cb(error, result);
}
let addDirs = (dir: DirectoryDTO) => { let done = (error: any, result: DirectoryDTO) => {
dir.photos.forEach((ph) => { if (error || !result) {
ph.directory = dir; return cb(error, result);
}); }
dir.directories.forEach((d) => {
addDirs(d); let addDirs = (dir: DirectoryDTO) => {
}); dir.photos.forEach((ph) => {
}; ph.directory = dir;
addDirs(result);
return cb(error, result);
}).on('error', (error) => {
return cb(error, null);
}); });
dir.directories.forEach((d) => {
addDirs(d);
});
};
addDirs(result);
return cb(error, result);
};
let error = (error) => {
return cb(error, null);
};
if (Config.Server.enableThreading == true) {
pool.send(input).on('done', done).on('error', error);
} else {
try {
diskManagerTask(input, done);
} catch (err) {
error(err);
}
} }
}
} }

View File

@@ -0,0 +1,192 @@
///<reference path="exif.d.ts"/>
import {DirectoryDTO} from "../../common/entities/DirectoryDTO";
import {
CameraMetadata,
GPSMetadata,
ImageSize,
PhotoDTO,
PhotoMetadata,
PositionMetaData
} from "../../common/entities/PhotoDTO";
const LOG_TAG = "[DiskManagerTask]";
export const diskManagerTask = (input: DiskManagerTask.PoolInput, done) => {
const fs = require("fs");
const path = require("path");
const mime = require("mime");
const iptc = require("node-iptc");
const exif_parser = require("exif-parser");
let isImage = (fullPath: string) => {
let imageMimeTypes = [
'image/bmp',
'image/gif',
'image/jpeg',
'image/png',
'image/pjpeg',
'image/tiff',
'image/webp',
'image/x-tiff',
'image/x-windows-bmp'
];
let extension = mime.lookup(fullPath);
return imageMimeTypes.indexOf(extension) !== -1;
};
let loadPhotoMetadata = (fullPath: string): Promise<PhotoMetadata> => {
return new Promise<PhotoMetadata>((resolve: (metadata: PhotoMetadata) => void, reject) => {
fs.readFile(fullPath, function (err, data) {
if (err) {
return reject({file: fullPath, error: err});
}
try {
const exif = exif_parser.create(data).parse();
const iptcData = iptc(data);
const imageSize: ImageSize = {width: exif.imageSize.width, height: exif.imageSize.height};
const cameraData: CameraMetadata = {
ISO: exif.tags.ISO,
model: exif.tags.Modeol,
maker: exif.tags.Make,
fStop: exif.tags.FNumber,
exposure: exif.tags.ExposureTime,
focalLength: exif.tags.FocalLength,
lens: exif.tags.LensModel,
};
const GPS: GPSMetadata = {
latitude: exif.tags.GPSLatitude,
longitude: exif.tags.GPSLongitude,
altitude: exif.tags.GPSAltitude
};
const positionData: PositionMetaData = {
GPSData: GPS,
country: iptcData.country_or_primary_location_name,
state: iptcData.province_or_state,
city: iptcData.city
};
//Decode characters to UTF8
const decode = (s: any) => {
for (let a, b, i = -1, l = (s = s.split("")).length, o = String.fromCharCode, c = "charCodeAt"; ++i < l;
((a = s[i][c](0)) & 0x80) &&
(s[i] = (a & 0xfc) == 0xc0 && ((b = s[i + 1][c](0)) & 0xc0) == 0x80 ?
o(((a & 0x03) << 6) + (b & 0x3f)) : o(128), s[++i] = "")
);
return s.join("");
};
const keywords: string[] = (iptcData.keywords || []).map((s: string) => decode(s));
const creationDate: number = iptcData.date_time ? iptcData.date_time.getTime() : 0;
const metadata: PhotoMetadata = <PhotoMetadata>{
keywords: keywords,
cameraData: cameraData,
positionData: positionData,
size: imageSize,
creationDate: creationDate
};
return resolve(metadata);
} catch (err) {
return reject({file: fullPath, error: err});
}
});
});
};
let parseDir = (directoryInfo: {
relativeDirectoryName: string,
directoryName: string,
directoryParent: string,
absoluteDirectoryName: string
}, maxPhotos: number = null, photosOnly: boolean = false): Promise<DirectoryDTO> => {
return new Promise<DirectoryDTO>((resolve, reject) => {
let promises: Array<Promise<any>> = [];
let directory = <DirectoryDTO>{
name: directoryInfo.directoryName,
path: directoryInfo.directoryParent,
lastUpdate: Date.now(),
directories: [],
photos: []
};
fs.readdir(directoryInfo.absoluteDirectoryName, (err, list) => {
if (err) {
return reject(err);
}
try {
for (let i = 0; i < list.length; i++) {
let file = list[i];
let fullFilePath = path.normalize(path.resolve(directoryInfo.absoluteDirectoryName, file));
if (photosOnly == false && fs.statSync(fullFilePath).isDirectory()) {
let promise = parseDir({
relativeDirectoryName: path.join(directoryInfo.relativeDirectoryName, path.sep),
directoryName: file,
directoryParent: path.join(directoryInfo.relativeDirectoryName, path.sep),
absoluteDirectoryName: fullFilePath
},
5, true
).then((dir) => {
directory.directories.push(dir);
});
promises.push(promise);
} else if (isImage(fullFilePath)) {
let promise = loadPhotoMetadata(fullFilePath).then((photoMetadata) => {
directory.photos.push(<PhotoDTO>{
name: file,
directory: null,
metadata: photoMetadata
});
});
promises.push(promise);
if (maxPhotos != null && promises.length > maxPhotos) {
break;
}
}
}
Promise.all(promises).then(() => {
return resolve(directory);
}).catch((err) => {
return reject({directoryInfo: directoryInfo, error: err});
});
} catch (err) {
return reject({directoryInfo: directoryInfo, error: err});
}
});
});
};
parseDir(input).then((dir) => {
done(null, dir);
}).catch((err) => {
done(err, null);
});
};
export module DiskManagerTask {
export interface PoolInput {
relativeDirectoryName: string;
directoryName: string;
directoryParent: string;
absoluteDirectoryName: string;
}
}

View File

@@ -5,70 +5,70 @@ import {MySQLConnection} from "./mysql/MySQLConnection";
export class ObjectManagerRepository { export class ObjectManagerRepository {
private _galleryManager: IGalleryManager; private _galleryManager: IGalleryManager;
private _userManager: IUserManager; private _userManager: IUserManager;
private _searchManager: ISearchManager; private _searchManager: ISearchManager;
private static _instance: ObjectManagerRepository = null; private static _instance: ObjectManagerRepository = null;
public static InitMemoryManagers() { public static InitMemoryManagers() {
const GalleryManager = require("./memory/GalleryManager").GalleryManager; const GalleryManager = require("./memory/GalleryManager").GalleryManager;
const UserManager = require("./memory/UserManager").UserManager; const UserManager = require("./memory/UserManager").UserManager;
const SearchManager = require("./memory/SearchManager").SearchManager; const SearchManager = require("./memory/SearchManager").SearchManager;
ObjectManagerRepository.getInstance().setGalleryManager(new GalleryManager());
ObjectManagerRepository.getInstance().setUserManager(new UserManager());
ObjectManagerRepository.getInstance().setSearchManager(new SearchManager());
}
public static InitMySQLManagers(): Promise<boolean> {
return new Promise<boolean>((resolve, reject) => {
MySQLConnection.init().then(() => {
const GalleryManager = require("./mysql/GalleryManager").GalleryManager;
const UserManager = require("./mysql/UserManager").UserManager;
const SearchManager = require("./mysql/SearchManager").SearchManager;
ObjectManagerRepository.getInstance().setGalleryManager(new GalleryManager()); ObjectManagerRepository.getInstance().setGalleryManager(new GalleryManager());
ObjectManagerRepository.getInstance().setUserManager(new UserManager()); ObjectManagerRepository.getInstance().setUserManager(new UserManager());
ObjectManagerRepository.getInstance().setSearchManager(new SearchManager()); ObjectManagerRepository.getInstance().setSearchManager(new SearchManager());
console.log("MySQL DB inited");
resolve(true);
}).catch(err => reject(err));
});
}
public static getInstance() {
if (this._instance === null) {
this._instance = new ObjectManagerRepository();
} }
return this._instance;
}
public static InitMySQLManagers(): Promise<boolean> { public static reset() {
return new Promise<boolean>((resolve, reject) => { this._instance = null;
MySQLConnection.init().then(() => { }
const GalleryManager = require("./mysql/GalleryManager").GalleryManager;
const UserManager = require("./mysql/UserManager").UserManager;
const SearchManager = require("./mysql/SearchManager").SearchManager;
ObjectManagerRepository.getInstance().setGalleryManager(new GalleryManager());
ObjectManagerRepository.getInstance().setUserManager(new UserManager());
ObjectManagerRepository.getInstance().setSearchManager(new SearchManager());
console.log("MySQL DB inited");
resolve(true);
}).catch(err => reject(err));
});
}
public static getInstance() {
if (this._instance === null) {
this._instance = new ObjectManagerRepository();
}
return this._instance;
}
public static reset() {
this._instance = null;
}
getGalleryManager(): IGalleryManager { getGalleryManager(): IGalleryManager {
return this._galleryManager; return this._galleryManager;
} }
setGalleryManager(value: IGalleryManager) { setGalleryManager(value: IGalleryManager) {
this._galleryManager = value; this._galleryManager = value;
} }
getUserManager(): IUserManager { getUserManager(): IUserManager {
return this._userManager; return this._userManager;
} }
setUserManager(value: IUserManager) { setUserManager(value: IUserManager) {
this._userManager = value; this._userManager = value;
} }
getSearchManager(): ISearchManager { getSearchManager(): ISearchManager {
return this._searchManager; return this._searchManager;
} }
setSearchManager(value: ISearchManager) { setSearchManager(value: ISearchManager) {
this._searchManager = value; this._searchManager = value;
} }
} }

View File

@@ -1,15 +1,15 @@
declare module "node-iptc" { declare module "node-iptc" {
function e(data):any; function e(data): any;
module e { module e {
} }
export = e; export = e;
} }
declare module "exif-parser" { declare module "exif-parser" {
export function create(data):any; export function create(data): any;
} }

View File

@@ -2,27 +2,27 @@ import {AuthenticationMWs} from "../middlewares/user/AuthenticationMWs";
import {UserRoles} from "../../common/entities/UserDTO"; import {UserRoles} from "../../common/entities/UserDTO";
export class AdminRouter { export class AdminRouter {
public static route(app: any) { public static route(app: any) {
this.addResetDB(app); this.addResetDB(app);
this.addIndexGallery(app); this.addIndexGallery(app);
} }
private static addResetDB(app) { private static addResetDB(app) {
app.post("/api/admin/db/reset", app.post("/api/admin/db/reset",
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin) AuthenticationMWs.authorise(UserRoles.Admin)
//TODO: implement //TODO: implement
); );
}; };
private static addIndexGallery(app) { private static addIndexGallery(app) {
app.post("/api/admin/gallery/index", app.post("/api/admin/gallery/index",
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin) AuthenticationMWs.authorise(UserRoles.Admin)
//TODO: implement //TODO: implement
); );
}; };
} }

View File

@@ -5,27 +5,27 @@ import Request = Express.Request;
import Response = Express.Response; import Response = Express.Response;
export class ErrorRouter { export class ErrorRouter {
public static route(app: any) { public static route(app: any) {
this.addApiErrorHandler(app); this.addApiErrorHandler(app);
this.addGenericHandler(app); this.addGenericHandler(app);
} }
private static addApiErrorHandler(app) { private static addApiErrorHandler(app) {
app.use("/api/*", app.use("/api/*",
RenderingMWs.renderError RenderingMWs.renderError
); );
}; };
private static addGenericHandler(app) { private static addGenericHandler(app) {
app.use((err: any, req: Request, res: Response, next: Function) => { app.use((err: any, req: Request, res: Response, next: Function) => {
//Flush out the stack to the console //Flush out the stack to the console
Logger.error(err); Logger.error(err);
next(new Error(ErrorCodes.SERVER_ERROR, "Unknown server side error")); next(new Error(ErrorCodes.SERVER_ERROR, "Unknown server side error"));
}, },
RenderingMWs.renderError RenderingMWs.renderError
); );
} }
} }

View File

@@ -4,82 +4,82 @@ import {RenderingMWs} from "../middlewares/RenderingMWs";
import {ThumbnailGeneratorMWs} from "../middlewares/thumbnail/ThumbnailGeneratorMWs"; import {ThumbnailGeneratorMWs} from "../middlewares/thumbnail/ThumbnailGeneratorMWs";
export class GalleryRouter { export class GalleryRouter {
public static route(app: any) { public static route(app: any) {
this.addGetImageIcon(app); this.addGetImageIcon(app);
this.addGetImageThumbnail(app); this.addGetImageThumbnail(app);
this.addGetImage(app); this.addGetImage(app);
this.addDirectoryList(app); this.addDirectoryList(app);
this.addSearch(app); this.addSearch(app);
this.addInstantSearch(app); this.addInstantSearch(app);
this.addAutoComplete(app); this.addAutoComplete(app);
} }
private static addDirectoryList(app) { private static addDirectoryList(app) {
app.get(["/api/gallery/content/:directory(*)", "/api/gallery/", "/api/gallery//"], app.get(["/api/gallery/content/:directory(*)", "/api/gallery/", "/api/gallery//"],
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
GalleryMWs.listDirectory, GalleryMWs.listDirectory,
ThumbnailGeneratorMWs.addThumbnailInformation, ThumbnailGeneratorMWs.addThumbnailInformation,
GalleryMWs.removeCyclicDirectoryReferences, GalleryMWs.removeCyclicDirectoryReferences,
RenderingMWs.renderResult RenderingMWs.renderResult
); );
}; };
private static addGetImage(app) { private static addGetImage(app) {
app.get(["/api/gallery/content/:imagePath(*\.(jpg|bmp|png|gif|jpeg))"], app.get(["/api/gallery/content/:imagePath(*\.(jpg|bmp|png|gif|jpeg))"],
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
GalleryMWs.loadImage, GalleryMWs.loadImage,
RenderingMWs.renderFile RenderingMWs.renderFile
); );
}; };
private static addGetImageThumbnail(app) { private static addGetImageThumbnail(app) {
app.get("/api/gallery/content/:imagePath(*\.(jpg|bmp|png|gif|jpeg))/thumbnail/:size?", app.get("/api/gallery/content/:imagePath(*\.(jpg|bmp|png|gif|jpeg))/thumbnail/:size?",
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
GalleryMWs.loadImage, GalleryMWs.loadImage,
ThumbnailGeneratorMWs.generateThumbnail, ThumbnailGeneratorMWs.generateThumbnail,
RenderingMWs.renderFile RenderingMWs.renderFile
); );
}; };
private static addGetImageIcon(app) { private static addGetImageIcon(app) {
app.get("/api/gallery/content/:imagePath(*\.(jpg|bmp|png|gif|jpeg))/icon", app.get("/api/gallery/content/:imagePath(*\.(jpg|bmp|png|gif|jpeg))/icon",
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
GalleryMWs.loadImage, GalleryMWs.loadImage,
ThumbnailGeneratorMWs.generateIcon, ThumbnailGeneratorMWs.generateIcon,
RenderingMWs.renderFile RenderingMWs.renderFile
); );
}; };
private static addSearch(app) { private static addSearch(app) {
app.get("/api/search/:text", app.get("/api/search/:text",
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
GalleryMWs.search, GalleryMWs.search,
ThumbnailGeneratorMWs.addThumbnailInformation, ThumbnailGeneratorMWs.addThumbnailInformation,
GalleryMWs.removeCyclicDirectoryReferences, GalleryMWs.removeCyclicDirectoryReferences,
RenderingMWs.renderResult RenderingMWs.renderResult
); );
}; };
private static addInstantSearch(app) { private static addInstantSearch(app) {
app.get("/api/instant-search/:text", app.get("/api/instant-search/:text",
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
GalleryMWs.instantSearch, GalleryMWs.instantSearch,
ThumbnailGeneratorMWs.addThumbnailInformation, ThumbnailGeneratorMWs.addThumbnailInformation,
GalleryMWs.removeCyclicDirectoryReferences, GalleryMWs.removeCyclicDirectoryReferences,
RenderingMWs.renderResult RenderingMWs.renderResult
); );
}; };
private static addAutoComplete(app) { private static addAutoComplete(app) {
app.get("/api/autocomplete/:text", app.get("/api/autocomplete/:text",
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
GalleryMWs.autocomplete, GalleryMWs.autocomplete,
RenderingMWs.renderResult RenderingMWs.renderResult
); );
}; };
} }

View File

@@ -6,32 +6,37 @@ import {Config} from "../../common/config/private/Config";
export class PublicRouter { export class PublicRouter {
public static route(app) { public static route(app) {
app.use((req: Request, res: Response, next: NextFunction) => { app.use((req: Request, res: Response, next: NextFunction) => {
res.tpl = {}; res.tpl = {};
res.tpl.user = null; res.tpl.user = null;
if (req.session.user) { if (req.session.user) {
let user = Utils.clone(req.session.user); let user = Utils.clone(req.session.user);
delete user.password; delete user.password;
res.tpl.user = user; res.tpl.user = user;
} }
res.tpl.clientConfig = Config.Client; res.tpl.clientConfig = Config.Client;
return next(); return next();
}); });
app.use(_express.static(_path.resolve(__dirname, './../../frontend'))); app.get('/config_inject.js', (req: Request, res: Response) => {
app.use('/node_modules', _express.static(_path.resolve(__dirname, './../../node_modules'))); res.render(_path.resolve(__dirname, './../../dist/config_inject.ejs'), res.tpl);
app.use('/common', _express.static(_path.resolve(__dirname, './../../common'))); });
app.get(['/', '/login', "/gallery*", "/admin", "/search*"], (req: Request, res: Response) => {
res.sendFile(_path.resolve(__dirname, './../../dist/index.html'));
});
const renderIndex = (req: Request, res: Response) => { app.use(_express.static(_path.resolve(__dirname, './../../dist')));
res.render(_path.resolve(__dirname, './../../frontend/index.ejs'), res.tpl); app.use('/node_modules', _express.static(_path.resolve(__dirname, './../../node_modules')));
}; app.use('/common', _express.static(_path.resolve(__dirname, './../../common')));
app.get(['/', '/login', "/gallery*", "/admin", "/search*"], renderIndex); const renderIndex = (req: Request, res: Response) => {
res.render(_path.resolve(__dirname, './../../dist/index.html'));
};
} }
} }

View File

@@ -2,27 +2,27 @@ import {AuthenticationMWs} from "../middlewares/user/AuthenticationMWs";
import {UserRoles} from "../../common/entities/UserDTO"; import {UserRoles} from "../../common/entities/UserDTO";
export class SharingRouter { export class SharingRouter {
public static route(app: any) { public static route(app: any) {
this.addGetSharing(app); this.addGetSharing(app);
this.addUpdateSharing(app); this.addUpdateSharing(app);
} }
private static addGetSharing(app) { private static addGetSharing(app) {
app.get("/api/share/:directory", app.get("/api/share/:directory",
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.User) AuthenticationMWs.authorise(UserRoles.User)
//TODO: implement //TODO: implement
); );
}; };
private static addUpdateSharing(app) { private static addUpdateSharing(app) {
app.post("/api/share/:directory", app.post("/api/share/:directory",
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.User) AuthenticationMWs.authorise(UserRoles.User)
//TODO: implement //TODO: implement
); );
}; };
} }

View File

@@ -5,92 +5,92 @@ import {UserRequestConstrainsMWs} from "../middlewares/user/UserRequestConstrain
import {RenderingMWs} from "../middlewares/RenderingMWs"; import {RenderingMWs} from "../middlewares/RenderingMWs";
export class UserRouter { export class UserRouter {
public static route(app) { public static route(app) {
this.addLogin(app); this.addLogin(app);
this.addLogout(app); this.addLogout(app);
this.addGetSessionUser(app); this.addGetSessionUser(app);
this.addChangePassword(app); this.addChangePassword(app);
this.addCreateUser(app); this.addCreateUser(app);
this.addDeleteUser(app); this.addDeleteUser(app);
this.addListUsers(app); this.addListUsers(app);
this.addChangeRole(app); this.addChangeRole(app);
} }
private static addLogin(app) { private static addLogin(app) {
app.post("/api/user/login", app.post("/api/user/login",
AuthenticationMWs.inverseAuthenticate, AuthenticationMWs.inverseAuthenticate,
AuthenticationMWs.login, AuthenticationMWs.login,
RenderingMWs.renderSessionUser RenderingMWs.renderSessionUser
); );
}; };
private static addLogout(app) { private static addLogout(app) {
app.post("/api/user/logout", app.post("/api/user/logout",
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
AuthenticationMWs.logout, AuthenticationMWs.logout,
RenderingMWs.renderOK RenderingMWs.renderOK
); );
}; };
private static addGetSessionUser(app) { private static addGetSessionUser(app) {
app.get("/api/user/login", app.get("/api/user/login",
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
RenderingMWs.renderSessionUser RenderingMWs.renderSessionUser
); );
}; };
private static addChangePassword(app) { private static addChangePassword(app) {
app.post("/api/user/:id/password", app.post("/api/user/:id/password",
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
UserRequestConstrainsMWs.forceSelfRequest, UserRequestConstrainsMWs.forceSelfRequest,
UserMWs.changePassword, UserMWs.changePassword,
RenderingMWs.renderOK RenderingMWs.renderOK
); );
}; };
private static addCreateUser(app) { private static addCreateUser(app) {
app.put("/api/user", app.put("/api/user",
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin), AuthenticationMWs.authorise(UserRoles.Admin),
UserMWs.createUser, UserMWs.createUser,
RenderingMWs.renderOK RenderingMWs.renderOK
); );
}; };
private static addDeleteUser(app) { private static addDeleteUser(app) {
app.delete("/api/user/:id", app.delete("/api/user/:id",
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin), AuthenticationMWs.authorise(UserRoles.Admin),
UserRequestConstrainsMWs.notSelfRequest, UserRequestConstrainsMWs.notSelfRequest,
UserMWs.deleteUser, UserMWs.deleteUser,
RenderingMWs.renderOK RenderingMWs.renderOK
); );
}; };
private static addListUsers(app) { private static addListUsers(app) {
app.get("/api/user/list", app.get("/api/user/list",
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin), AuthenticationMWs.authorise(UserRoles.Admin),
UserMWs.listUsers, UserMWs.listUsers,
RenderingMWs.renderResult RenderingMWs.renderResult
); );
}; };
private static addChangeRole(app) { private static addChangeRole(app) {
app.post("/api/user/:id/role", app.post("/api/user/:id/role",
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
AuthenticationMWs.authorise(UserRoles.Admin), AuthenticationMWs.authorise(UserRoles.Admin),
UserRequestConstrainsMWs.notSelfRequestOr2Admins, UserRequestConstrainsMWs.notSelfRequestOr2Admins,
UserMWs.changeRole, UserMWs.changeRole,
RenderingMWs.renderOK RenderingMWs.renderOK
); );
}; };
} }

View File

@@ -18,170 +18,170 @@ import {DatabaseType} from "../common/config/private/IPrivateConfig";
const LOG_TAG = "[server]"; const LOG_TAG = "[server]";
export class Server { export class Server {
private debug: any; private debug: any;
private app: any; private app: any;
private server: any; private server: any;
constructor() { constructor() {
this.init(); this.init();
} }
async init() { async init() {
Logger.info(LOG_TAG, "config:"); Logger.info(LOG_TAG, "config:");
Logger.info(LOG_TAG, JSON.stringify(Config, null, '\t')); Logger.info(LOG_TAG, JSON.stringify(Config, null, '\t'));
this.app = _express(); this.app = _express();
this.app.use(expressWinston.logger({ this.app.use(expressWinston.logger({
transports: [ transports: [
new winston.transports.Console({ new winston.transports.Console({
level: 'silly', level: 'silly',
json: false, json: false,
colorize: true, colorize: true,
timestamp: function () { timestamp: function () {
return (new Date()).toLocaleString(); return (new Date()).toLocaleString();
}, },
formatter: (options) => { formatter: (options) => {
// Return string will be passed to logger. // Return string will be passed to logger.
return options.timestamp() + '[' + winston['config']['colorize'](options.level, options.level.toUpperCase()) + '] ' + return options.timestamp() + '[' + winston['config']['colorize'](options.level, options.level.toUpperCase()) + '] ' +
(undefined !== options.message ? options.message : '') + (undefined !== options.message ? options.message : '') +
(options.meta && Object.keys(options.meta).length ? '\n\t' + JSON.stringify(options.meta) : '' ); (options.meta && Object.keys(options.meta).length ? '\n\t' + JSON.stringify(options.meta) : '' );
}, },
debugStdout: true debugStdout: true
}) })
], ],
meta: false, // optional: control whether you want to log the meta data about the request (default to true) meta: false, // optional: control whether you want to log the meta data about the request (default to true)
msg: "HTTP {{req.method}} {{req.url}}", // optional: customize the default logging message. E.g. "{{res.statusCode}} {{req.method}} {{res.responseTime}}ms {{req.url}}" msg: "HTTP {{req.method}} {{req.url}}", // optional: customize the default logging message. E.g. "{{res.statusCode}} {{req.method}} {{res.responseTime}}ms {{req.url}}"
expressFormat: true, // Use the default Express/morgan request formatting. Enabling this will override any msg if true. Will only output colors with colorize set to true expressFormat: true, // Use the default Express/morgan request formatting. Enabling this will override any msg if true. Will only output colors with colorize set to true
colorize: true, // Color the text and status code, using the Express/morgan color palette (text: gray, status: default green, 3XX cyan, 4XX yellow, 5XX red). colorize: true, // Color the text and status code, using the Express/morgan color palette (text: gray, status: default green, 3XX cyan, 4XX yellow, 5XX red).
level: (req) => { level: (req) => {
if (req.url.indexOf("/api/") !== -1) { if (req.url.indexOf("/api/") !== -1) {
return "verbose"; return "verbose";
}
return req.url.indexOf("node_modules") !== -1 ? "silly" : "debug"
}
}));
this.app.set('view engine', 'ejs');
/**
* Session above all
*/
this.app.use(_session({
name: "pigallery2-session",
secret: 'PiGallery2 secret',
cookie: {
maxAge: 60000 * 10,
httpOnly: false
},
resave: true,
saveUninitialized: false
}));
/**
* Parse parameters in POST
*/
// for parsing application/json
this.app.use(_bodyParser.json());
if (Config.Server.database.type == DatabaseType.mysql) {
try {
await ObjectManagerRepository.InitMySQLManagers();
} catch (err) {
Logger.warn(LOG_TAG, "[MYSQL error]", err);
Logger.warn(LOG_TAG, "Error during initializing mysql falling back to memory DB");
Config.setDatabaseType(DatabaseType.memory);
await ObjectManagerRepository.InitMemoryManagers();
}
} else {
await ObjectManagerRepository.InitMemoryManagers();
} }
return req.url.indexOf("node_modules") !== -1 ? "silly" : "debug"
}
}));
if (Config.Server.thumbnail.hardwareAcceleration == true) { this.app.set('view engine', 'ejs');
try {
const sharp = require("sharp");
sharp();
} catch (err) {
Logger.warn(LOG_TAG, "[Thumbnail hardware acceleration] sharp module error: ", err);
Logger.warn(LOG_TAG, "Thumbnail hardware acceleration is not possible." +
" 'Sharp' node module is not found." +
" Falling back to JS based thumbnail generation");
Config.Server.thumbnail.hardwareAcceleration = false;
}
}
PublicRouter.route(this.app);
UserRouter.route(this.app);
GalleryRouter.route(this.app);
SharingRouter.route(this.app);
AdminRouter.route(this.app);
ErrorRouter.route(this.app);
// Get PORT from environment and store in Express.
this.app.set('port', Config.Server.port);
// Create HTTP server.
this.server = _http.createServer(this.app);
//Listen on provided PORT, on all network interfaces.
this.server.listen(Config.Server.port);
this.server.on('error', this.onError);
this.server.on('listening', this.onListening);
}
/** /**
* Event listener for HTTP server "error" event. * Session above all
*/ */
private onError = (error: any) => { this.app.use(_session({
if (error.syscall !== 'listen') { name: "pigallery2-session",
throw error; secret: 'PiGallery2 secret',
} cookie: {
maxAge: 60000 * 10,
const bind = typeof Config.Server.port === 'string' httpOnly: false
? 'Pipe ' + Config.Server.port },
: 'Port ' + Config.Server.port; resave: true,
saveUninitialized: false
// handle specific listen error with friendly messages }));
switch (error.code) {
case 'EACCES':
Logger.error(LOG_TAG, bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
Logger.error(LOG_TAG, bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
};
/** /**
* Event listener for HTTP server "listening" event. * Parse parameters in POST
*/ */
private onListening = () => { // for parsing application/json
let addr = this.server.address(); this.app.use(_bodyParser.json());
const bind = typeof addr === 'string'
? 'pipe ' + addr if (Config.Server.database.type == DatabaseType.mysql) {
: 'port ' + addr.port; try {
Logger.info(LOG_TAG, 'Listening on ' + bind); await ObjectManagerRepository.InitMySQLManagers();
}; } catch (err) {
Logger.warn(LOG_TAG, "[MYSQL error]", err);
Logger.warn(LOG_TAG, "Error during initializing mysql falling back to memory DB");
Config.setDatabaseType(DatabaseType.memory);
await ObjectManagerRepository.InitMemoryManagers();
}
} else {
await ObjectManagerRepository.InitMemoryManagers();
}
if (Config.Server.thumbnail.hardwareAcceleration == true) {
try {
const sharp = require("sharp");
sharp();
} catch (err) {
Logger.warn(LOG_TAG, "[Thumbnail hardware acceleration] sharp module error: ", err);
Logger.warn(LOG_TAG, "Thumbnail hardware acceleration is not possible." +
" 'Sharp' node module is not found." +
" Falling back to JS based thumbnail generation");
Config.Server.thumbnail.hardwareAcceleration = false;
}
}
PublicRouter.route(this.app);
UserRouter.route(this.app);
GalleryRouter.route(this.app);
SharingRouter.route(this.app);
AdminRouter.route(this.app);
ErrorRouter.route(this.app);
// Get PORT from environment and store in Express.
this.app.set('port', Config.Server.port);
// Create HTTP server.
this.server = _http.createServer(this.app);
//Listen on provided PORT, on all network interfaces.
this.server.listen(Config.Server.port);
this.server.on('error', this.onError);
this.server.on('listening', this.onListening);
}
/**
* Event listener for HTTP server "error" event.
*/
private onError = (error: any) => {
if (error.syscall !== 'listen') {
throw error;
}
const bind = typeof Config.Server.port === 'string'
? 'Pipe ' + Config.Server.port
: 'Port ' + Config.Server.port;
// handle specific listen error with friendly messages
switch (error.code) {
case 'EACCES':
Logger.error(LOG_TAG, bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
Logger.error(LOG_TAG, bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
};
/**
* Event listener for HTTP server "listening" event.
*/
private onListening = () => {
let addr = this.server.address();
const bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
Logger.info(LOG_TAG, 'Listening on ' + bind);
};
} }
if (process.env.DEBUG) { if (process.env.DEBUG) {
Logger.debug(LOG_TAG, "Running in DEBUG mode"); Logger.debug(LOG_TAG, "Running in DEBUG mode");
} }
new Server(); new Server();

View File

@@ -1,14 +1,14 @@
export var MessageTypes = { export var MessageTypes = {
Client: { Client: {
Login: { Login: {
Authenticate: "Authenticate" Authenticate: "Authenticate"
}
},
Server: {
Login: {
Authenticated: "Authenticated"
}
} }
};
},
Server: {
Login: {
Authenticated: "Authenticated"
}
}
};

View File

@@ -1,107 +1,107 @@
export class Utils { export class Utils {
static clone<T>(object: T): T { static clone<T>(object: T): T {
return JSON.parse(JSON.stringify(object)); return JSON.parse(JSON.stringify(object));
}
static equalsFilter(object: any, filter: any): boolean {
let keys = Object.keys(filter);
for (let i = 0; i < keys.length; i++) {
let key = keys[i];
if (object[key] !== filter[key]) {
return false;
}
} }
static equalsFilter(object: any, filter: any): boolean { return true;
}
let keys = Object.keys(filter);
for (let i = 0; i < keys.length; i++) { static concatUrls(...args: Array<string>) {
let key = keys[i]; let url = "";
if (object[key] !== filter[key]) { for (let i = 0; i < args.length; i++) {
return false; if (args[i] === "" || typeof args[i] === "undefined") continue;
}
let part = args[i].replace("\\", "/");
if (part === "/" || part === "./") continue;
url += part + "/";
}
url = url.replace("//", "/");
return url.substring(0, url.length - 1);
}
public static updateKeys(targetObject: any, sourceObject: any) {
Object.keys(sourceObject).forEach((key) => {
if (typeof targetObject[key] === "undefined") {
return;
}
if (typeof targetObject[key] === "object") {
Utils.updateKeys(targetObject[key], sourceObject[key]);
} else {
targetObject[key] = sourceObject[key];
}
});
}
public static setKeys(targetObject: any, sourceObject: any) {
Object.keys(sourceObject).forEach((key) => {
if (typeof targetObject[key] === "object") {
Utils.setKeys(targetObject[key], sourceObject[key]);
} else {
targetObject[key] = sourceObject[key];
}
});
}
public static setKeysForced(targetObject: any, sourceObject: any) {
Object.keys(sourceObject).forEach((key) => {
if (typeof sourceObject[key] === "object") {
if (typeof targetObject[key] === "undefined") {
targetObject[key] = {};
} }
Utils.setKeysForced(targetObject[key], sourceObject[key]);
} else {
targetObject[key] = sourceObject[key];
}
});
}
return true; public static enumToArray(EnumType: any): Array<{ key: number; value: string; }> {
let arr: Array<{ key: number; value: string; }> = [];
for (let enumMember in EnumType) {
if (!EnumType.hasOwnProperty(enumMember)) {
continue;
}
let key = parseInt(enumMember, 10);
if (key >= 0) {
arr.push({key: key, value: EnumType[enumMember]});
}
} }
return arr;
}
static concatUrls(...args: Array<string>) { public static findClosest(number: number, arr: Array<number>) {
let url = "";
for (let i = 0; i < args.length; i++) {
if (args[i] === "" || typeof args[i] === "undefined") continue;
let part = args[i].replace("\\", "/"); let curr = arr[0];
if (part === "/" || part === "./") continue; let diff = Math.abs(number - curr);
url += part + "/"; arr.forEach((value) => {
}
url = url.replace("//", "/");
return url.substring(0, url.length - 1); let newDiff = Math.abs(number - value);
}
public static updateKeys(targetObject: any, sourceObject: any) { if (newDiff < diff) {
Object.keys(sourceObject).forEach((key) => { diff = newDiff;
if (typeof targetObject[key] === "undefined") { curr = value;
return; }
}
if (typeof targetObject[key] === "object") {
Utils.updateKeys(targetObject[key], sourceObject[key]);
} else {
targetObject[key] = sourceObject[key];
}
});
}
public static setKeys(targetObject: any, sourceObject: any) { });
Object.keys(sourceObject).forEach((key) => {
if (typeof targetObject[key] === "object") {
Utils.setKeys(targetObject[key], sourceObject[key]);
} else {
targetObject[key] = sourceObject[key];
}
});
}
public static setKeysForced(targetObject: any, sourceObject: any) { return curr;
Object.keys(sourceObject).forEach((key) => { }
if (typeof sourceObject[key] === "object") {
if (typeof targetObject[key] === "undefined") {
targetObject[key] = {};
}
Utils.setKeysForced(targetObject[key], sourceObject[key]);
} else {
targetObject[key] = sourceObject[key];
}
});
}
public static enumToArray(EnumType: any): Array<{key: number;value: string;}> {
let arr: Array<{key: number;value: string;}> = [];
for (let enumMember in EnumType) {
if (!EnumType.hasOwnProperty(enumMember)) {
continue;
}
let key = parseInt(enumMember, 10);
if (key >= 0) {
arr.push({key: key, value: EnumType[enumMember]});
}
}
return arr;
}
public static findClosest(number: number, arr: Array<number>) {
let curr = arr[0];
let diff = Math.abs(number - curr);
arr.forEach((value) => {
let newDiff = Math.abs(number - value);
if (newDiff < diff) {
diff = newDiff;
curr = value;
}
});
return curr;
}
} }

View File

@@ -26,4 +26,5 @@ export interface ServerConfig {
imagesFolder: string; imagesFolder: string;
thumbnail: ThumbnailConfig; thumbnail: ThumbnailConfig;
database: DataBaseConfig; database: DataBaseConfig;
enableThreading:boolean;
} }

View File

@@ -24,7 +24,8 @@ export class PrivateConfigClass extends PublicConfigClass {
database: "pigallery2" database: "pigallery2"
} }
} },
enableThreading: true
}; };
public setDatabaseType(type: DatabaseType) { public setDatabaseType(type: DatabaseType) {

View File

@@ -1,16 +1,16 @@
export enum SearchTypes { export enum SearchTypes {
directory = 1, directory = 1,
keyword = 2, keyword = 2,
position = 3, position = 3,
image = 4 image = 4
} }
export class AutoCompleteItem { export class AutoCompleteItem {
constructor(public text:string, public type:SearchTypes) { constructor(public text: string, public type: SearchTypes) {
} }
equals(other:AutoCompleteItem) { equals(other: AutoCompleteItem) {
return this.text === other.text && this.type === other.type; return this.text === other.text && this.type === other.type;
} }
} }

View File

@@ -2,12 +2,12 @@ import {DirectoryDTO} from "./DirectoryDTO";
import {SearchResultDTO} from "./SearchResult"; import {SearchResultDTO} from "./SearchResult";
export class ContentWrapper { export class ContentWrapper {
public directory: DirectoryDTO; public directory: DirectoryDTO;
public searchResult: SearchResultDTO; public searchResult: SearchResultDTO;
constructor(directory: DirectoryDTO = null, searchResult: SearchResultDTO = null) { constructor(directory: DirectoryDTO = null, searchResult: SearchResultDTO = null) {
this.directory = directory; this.directory = directory;
this.searchResult = searchResult; this.searchResult = searchResult;
} }
} }

View File

@@ -1,11 +1,11 @@
import {PhotoDTO} from "./PhotoDTO"; import {PhotoDTO} from "./PhotoDTO";
export interface DirectoryDTO { export interface DirectoryDTO {
id: number; id: number;
name: string; name: string;
path: string; path: string;
lastUpdate: number; lastUpdate: number;
parent: DirectoryDTO; parent: DirectoryDTO;
directories: Array<DirectoryDTO>; directories: Array<DirectoryDTO>;
photos: Array<PhotoDTO>; photos: Array<PhotoDTO>;
} }

View File

@@ -1,22 +1,22 @@
export enum ErrorCodes{ export enum ErrorCodes{
NOT_AUTHENTICATED = 0, NOT_AUTHENTICATED = 0,
ALREADY_AUTHENTICATED = 1, ALREADY_AUTHENTICATED = 1,
NOT_AUTHORISED = 2, NOT_AUTHORISED = 2,
CREDENTIAL_NOT_FOUND = 3, CREDENTIAL_NOT_FOUND = 3,
USER_CREATION_ERROR = 4, USER_CREATION_ERROR = 4,
GENERAL_ERROR = 5, GENERAL_ERROR = 5,
THUMBNAIL_GENERATION_ERROR = 6, THUMBNAIL_GENERATION_ERROR = 6,
SERVER_ERROR = 7, SERVER_ERROR = 7,
USER_MANAGEMENT_DISABLED = 8 USER_MANAGEMENT_DISABLED = 8
} }
export class Error { export class Error {
constructor(public code: ErrorCodes, public message?: string) { constructor(public code: ErrorCodes, public message?: string) {
} }
} }

View File

@@ -1,5 +1,5 @@
export class LoginCredential { export class LoginCredential {
constructor(public username:string = "", public password:string = "") { constructor(public username: string = "", public password: string = "") {
} }
} }

View File

@@ -1,11 +1,11 @@
import {Error} from "./Error"; import {Error} from "./Error";
export class Message<T> { export class Message<T> {
public error:Error = null; public error: Error = null;
public result:T = null; public result: T = null;
constructor(error:Error, result:T) { constructor(error: Error, result: T) {
this.error = error; this.error = error;
this.result = result; this.result = result;
} }
} }

View File

@@ -2,7 +2,7 @@ import {UserModificationRequest} from "./UserModificationRequest";
export class PasswordChangeRequest extends UserModificationRequest { export class PasswordChangeRequest extends UserModificationRequest {
constructor(id:number, public oldPassword:string, public newPassword:string) { constructor(id: number, public oldPassword: string, public newPassword: string) {
super(id); super(id);
} }
} }

View File

@@ -1,47 +1,47 @@
import {DirectoryDTO} from "./DirectoryDTO"; import {DirectoryDTO} from "./DirectoryDTO";
export interface PhotoDTO { export interface PhotoDTO {
id: number; id: number;
name: string; name: string;
directory: DirectoryDTO; directory: DirectoryDTO;
metadata: PhotoMetadata; metadata: PhotoMetadata;
readyThumbnails: Array<number>; readyThumbnails: Array<number>;
readyIcon: boolean; readyIcon: boolean;
} }
export interface PhotoMetadata { export interface PhotoMetadata {
keywords: Array<string>; keywords: Array<string>;
cameraData: CameraMetadata; cameraData: CameraMetadata;
positionData: PositionMetaData; positionData: PositionMetaData;
size: ImageSize; size: ImageSize;
creationDate: number; creationDate: number;
} }
export interface ImageSize { export interface ImageSize {
width: number; width: number;
height: number; height: number;
} }
export interface CameraMetadata { export interface CameraMetadata {
ISO?: number; ISO?: number;
model?: string; model?: string;
maker?: string; maker?: string;
fStop?: number; fStop?: number;
exposure?: number; exposure?: number;
focalLength?: number; focalLength?: number;
lens?: string; lens?: string;
} }
export interface PositionMetaData { export interface PositionMetaData {
GPSData?: GPSMetadata; GPSData?: GPSMetadata;
country?: string; country?: string;
state?: string; state?: string;
city?: string; city?: string;
} }
export interface GPSMetadata { export interface GPSMetadata {
latitude?: number; latitude?: number;
longitude?: number; longitude?: number;
altitude?: string; altitude?: string;
} }

View File

@@ -2,8 +2,8 @@ import {DirectoryDTO} from "./DirectoryDTO";
import {PhotoDTO} from "./PhotoDTO"; import {PhotoDTO} from "./PhotoDTO";
import {SearchTypes} from "./AutoCompleteItem"; import {SearchTypes} from "./AutoCompleteItem";
export interface SearchResultDTO { export interface SearchResultDTO {
searchText: string; searchText: string;
searchType: SearchTypes; searchType: SearchTypes;
directories: Array<DirectoryDTO>; directories: Array<DirectoryDTO>;
photos: Array<PhotoDTO>; photos: Array<PhotoDTO>;
} }

View File

@@ -1,14 +1,14 @@
export enum UserRoles{ export enum UserRoles{
Guest = 1, Guest = 1,
User = 2, User = 2,
Admin = 3, Admin = 3,
Developer = 4, Developer = 4,
} }
export interface UserDTO { export interface UserDTO {
id: number; id: number;
name: string; name: string;
password: string; password: string;
role: UserRoles; role: UserRoles;
} }

View File

@@ -1,4 +1,4 @@
export class UserModificationRequest { export class UserModificationRequest {
constructor(public id:number) { constructor(public id: number) {
} }
} }

View File

@@ -1,30 +1,30 @@
function isFunction(functionToCheck: any) { function isFunction(functionToCheck: any) {
let getType = {}; let getType = {};
return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]'; return functionToCheck && getType.toString.call(functionToCheck) === '[object Function]';
} }
export class Event<T> { export class Event<T> {
private handlers: {(data?: T): void;}[] = []; private handlers: { (data?: T): void; }[] = [];
public on(handler: {(data?: T): void}) { public on(handler: { (data?: T): void }) {
if (!isFunction(handler)) { if (!isFunction(handler)) {
throw new Error("Handler is not a function"); throw new Error("Handler is not a function");
}
this.handlers.push(handler);
} }
this.handlers.push(handler);
}
public off(handler: {(data?: T): void}) { public off(handler: { (data?: T): void }) {
this.handlers = this.handlers.filter(h => h !== handler); this.handlers = this.handlers.filter(h => h !== handler);
} }
public allOff() { public allOff() {
this.handlers = []; this.handlers = [];
} }
public trigger(data?: T) { public trigger(data?: T) {
if (this.handlers) { if (this.handlers) {
this.handlers.slice(0).forEach(h => h(data)); this.handlers.slice(0).forEach(h => h(data));
}
} }
}
} }

View File

@@ -1,21 +1,21 @@
export class Event2Args<T,M> { export class Event2Args<T, M> {
private handlers: { (data?: T,data2?: M): void; }[] = []; private handlers: { (data?: T, data2?: M): void; }[] = [];
public on(handler: { (data?: T,data2?: M): void }) { public on(handler: { (data?: T, data2?: M): void }) {
this.handlers.push(handler); this.handlers.push(handler);
} }
public off(handler: { (data?: T,data2?: M): void }) { public off(handler: { (data?: T, data2?: M): void }) {
this.handlers = this.handlers.filter(h => h !== handler); this.handlers = this.handlers.filter(h => h !== handler);
} }
public allOff() { public allOff() {
this.handlers = []; this.handlers = [];
} }
public trigger(data?: T,data2?: M) { public trigger(data?: T, data2?: M) {
if (this.handlers) { if (this.handlers) {
this.handlers.slice(0).forEach(h => h(data,data2)); this.handlers.slice(0).forEach(h => h(data, data2));
}
} }
}
} }

View File

@@ -1,70 +1,74 @@
export class EventLimit<T> { export class EventLimit<T> {
private lastTriggerValue:T = null; private lastTriggerValue: T = null;
private handlers:Array<EventLimitHandler<T>> = []; private handlers: Array<EventLimitHandler<T>> = [];
public on(limit:T, handler:{ (data?:T): void }) { public on(limit: T, handler: { (data?: T): void }) {
this.handlers.push(new EventLimitHandler(limit, handler)); this.handlers.push(new EventLimitHandler(limit, handler));
if (this.lastTriggerValue != null) { if (this.lastTriggerValue != null) {
this.trigger(this.lastTriggerValue); this.trigger(this.lastTriggerValue);
}
}
public onSingle(limit: T, handler: { (data?: T): void }) {
this.handlers.push(new SingleFireEventLimitHandler(limit, handler));
if (this.lastTriggerValue != null) {
this.trigger(this.lastTriggerValue);
}
}
public off(limit: T, handler: { (data?: T): void }) {
this.handlers = this.handlers.filter(h => h.handler !== handler && h.limit !== limit);
}
public allOff() {
this.handlers = [];
}
public trigger = (data?: T) => {
if (this.handlers) {
this.handlers.slice(0).forEach(h => {
if (h.limit <= data && (h.lastTriggerValue < h.limit || h.lastTriggerValue == null)) {
h.fire(data);
} }
h.lastTriggerValue = data;
});
this.handlers = this.handlers.filter(h => h.isValid());
} }
this.lastTriggerValue = data;
public onSingle(limit:T, handler:{ (data?:T): void }) { };
this.handlers.push(new SingleFireEventLimitHandler(limit, handler));
if (this.lastTriggerValue != null) {
this.trigger(this.lastTriggerValue);
}
}
public off(limit:T, handler:{ (data?:T): void }) {
this.handlers = this.handlers.filter(h => h.handler !== handler && h.limit !== limit);
}
public allOff() {
this.handlers = [];
}
public trigger = (data?:T) => {
if (this.handlers) {
this.handlers.slice(0).forEach(h => {
if (h.limit <= data && (h.lastTriggerValue < h.limit || h.lastTriggerValue == null)) {
h.fire(data);
}
h.lastTriggerValue = data;
});
this.handlers = this.handlers.filter(h => h.isValid());
}
this.lastTriggerValue = data;
};
} }
class EventLimitHandler<T> { class EventLimitHandler<T> {
public lastTriggerValue:T = null; public lastTriggerValue: T = null;
constructor(public limit:T, public handler:{ (data?:T): void }) {
}
public fire(data?: T){
this.handler(data);
}
public isValid():boolean{
return true;
}
}
class SingleFireEventLimitHandler<T> extends EventLimitHandler<T>{
public fired = false;
constructor(public limit:T, public handler:{ (data?:T): void }) {
super(limit,handler);
}
public fire(data?: T){ constructor(public limit: T, public handler: { (data?: T): void }) {
if(this.fired == false) { }
this.handler(data);
}
this.fired = true
}
public isValid():boolean{ public fire(data?: T) {
return this.fired === false; this.handler(data);
} }
public isValid(): boolean {
return true;
}
}
class SingleFireEventLimitHandler<T> extends EventLimitHandler<T> {
public fired = false;
constructor(public limit: T, public handler: { (data?: T): void }) {
super(limit, handler);
}
public fire(data?: T) {
if (this.fired == false) {
this.handler(data);
}
this.fired = true
}
public isValid(): boolean {
return this.fired === false;
}
} }

View File

@@ -4,23 +4,23 @@ import {Router} from "@angular/router";
import {UserRoles} from "../../../common/entities/UserDTO"; import {UserRoles} from "../../../common/entities/UserDTO";
import {Config} from "../../../common/config/public/Config"; import {Config} from "../../../common/config/public/Config";
@Component({ @Component({
selector: 'admin', selector: 'admin',
templateUrl: 'app/admin/admin.component.html', templateUrl: './admin.component.html',
styleUrls: ['app/admin/admin.component.css'] styleUrls: ['./admin.component.css']
}) })
export class AdminComponent implements OnInit { export class AdminComponent implements OnInit {
userManagementEnable: boolean = false; userManagementEnable: boolean = false;
constructor(private _authService: AuthenticationService, private _router: Router) { constructor(private _authService: AuthenticationService, private _router: Router) {
this.userManagementEnable = Config.Client.authenticationRequired; this.userManagementEnable = Config.Client.authenticationRequired;
} }
ngOnInit() { ngOnInit() {
if (!this._authService.isAuthenticated() || this._authService.getUser().role < UserRoles.Admin) { if (!this._authService.isAuthenticated() || this._authService.getUser().role < UserRoles.Admin) {
this._router.navigate(['login']); this._router.navigate(['login']);
return; return;
}
} }
}
} }

View File

@@ -4,31 +4,31 @@ import {UserDTO} from "../../common/entities/UserDTO";
import {Router} from "@angular/router"; import {Router} from "@angular/router";
@Component({ @Component({
selector: 'pi-gallery2-app', selector: 'pi-gallery2-app',
template: `<router-outlet></router-outlet>`, template: `<router-outlet></router-outlet>`,
}) })
export class AppComponent implements OnInit { export class AppComponent implements OnInit {
constructor(private _router: Router, private _authenticationService: AuthenticationService) { constructor(private _router: Router, private _authenticationService: AuthenticationService) {
} }
ngOnInit() { ngOnInit() {
this._authenticationService.OnUserChanged.on((user: UserDTO) => { this._authenticationService.OnUserChanged.on((user: UserDTO) => {
if (user != null) { if (user != null) {
if (this._router.isActive('login', true)) { if (this._router.isActive('login', true)) {
console.log("routing"); console.log("routing");
this._router.navigate(["gallery", ""]); this._router.navigate(["gallery", ""]);
} }
} else { } else {
if (this._router.isActive('login', true)) { if (this._router.isActive('login', true)) {
console.log("routing"); console.log("routing");
this._router.navigate(["login"]); this._router.navigate(["login"]);
} }
} }
}); });
} }
} }

View File

@@ -1,4 +1,4 @@
import {NgModule} from "@angular/core"; import {Injectable, NgModule} from "@angular/core";
import {BrowserModule} from "@angular/platform-browser"; import {BrowserModule} from "@angular/platform-browser";
import {FormsModule} from "@angular/forms"; import {FormsModule} from "@angular/forms";
import {HttpModule} from "@angular/http"; import {HttpModule} from "@angular/http";
@@ -31,48 +31,58 @@ import {GalleryMapLightboxComponent} from "./gallery/map/lightbox/lightbox.map.g
import {ThumbnailManagerService} from "./gallery/thumnailManager.service"; import {ThumbnailManagerService} from "./gallery/thumnailManager.service";
import {OverlayService} from "./gallery/overlay.service"; import {OverlayService} from "./gallery/overlay.service";
import {Config} from "../../common/config/public/Config"; import {Config} from "../../common/config/public/Config";
import {LAZY_MAPS_API_CONFIG} from "@agm/core/services";
@Injectable()
export class GoogleMapsConfig {
apiKey: string;
constructor() {
this.apiKey = Config.Client.googleApiKey;
}
}
@NgModule({ @NgModule({
imports: [ imports: [
BrowserModule, BrowserModule,
FormsModule, FormsModule,
HttpModule, HttpModule,
appRoutes, appRoutes,
AgmCoreModule.forRoot({ AgmCoreModule.forRoot()
apiKey: Config.Client.googleApiKey ],
}) declarations: [AppComponent,
], LoginComponent,
declarations: [AppComponent, AdminComponent,
LoginComponent, GalleryComponent,
AdminComponent, FrameComponent,
GalleryComponent, UserMangerSettingsComponent,
FrameComponent, GalleryLightboxPhotoComponent,
UserMangerSettingsComponent, GalleryPhotoLoadingComponent,
GalleryLightboxPhotoComponent, GalleryGridComponent,
GalleryPhotoLoadingComponent, GalleryDirectoryComponent,
GalleryGridComponent, GalleryLightboxComponent,
GalleryDirectoryComponent, GalleryMapComponent,
GalleryLightboxComponent, GalleryMapLightboxComponent,
GalleryMapComponent, FrameComponent,
GalleryMapLightboxComponent, GallerySearchComponent,
FrameComponent, GalleryNavigatorComponent,
GallerySearchComponent, GalleryPhotoComponent,
GalleryNavigatorComponent, FrameComponent,
GalleryPhotoComponent, StringifyRole],
FrameComponent, providers: [
StringifyRole], {provide: LAZY_MAPS_API_CONFIG, useClass: GoogleMapsConfig},
providers: [ NetworkService,
NetworkService, UserService,
UserService, GalleryCacheService,
GalleryCacheService, GalleryService,
GalleryService, AuthenticationService,
AuthenticationService, ThumbnailLoaderService,
ThumbnailLoaderService, ThumbnailManagerService,
ThumbnailManagerService, FullScreenService,
FullScreenService, OverlayService],
OverlayService],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })
export class AppModule { export class AppModule {
} }

View File

@@ -1,31 +1,31 @@
import {ModuleWithProviders} from "@angular/core"; import {ModuleWithProviders} from "@angular/core";
import {Routes, RouterModule} from "@angular/router"; import {RouterModule, Routes} from "@angular/router";
import {LoginComponent} from "./login/login.component"; import {LoginComponent} from "./login/login.component";
import {GalleryComponent} from "./gallery/gallery.component"; import {GalleryComponent} from "./gallery/gallery.component";
import {AdminComponent} from "./admin/admin.component"; import {AdminComponent} from "./admin/admin.component";
const ROUTES: Routes = [ const ROUTES: Routes = [
{ {
path: 'login', path: 'login',
component: LoginComponent component: LoginComponent
}, },
{ {
path: 'admin', path: 'admin',
component: AdminComponent component: AdminComponent
}, },
{ {
path: 'gallery/:directory', path: 'gallery/:directory',
component: GalleryComponent component: GalleryComponent
}, },
{ {
path: 'gallery', path: 'gallery',
component: GalleryComponent component: GalleryComponent
}, },
{ {
path: 'search/:searchText', path: 'search/:searchText',
component: GalleryComponent component: GalleryComponent
}, },
{path: '', redirectTo: '/login', pathMatch: 'full'} {path: '', redirectTo: '/login', pathMatch: 'full'}
]; ];
export const appRoutes: ModuleWithProviders = RouterModule.forRoot(ROUTES); export const appRoutes: ModuleWithProviders = RouterModule.forRoot(ROUTES);

View File

@@ -8,7 +8,7 @@
<span class="icon-bar"></span> <span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar"></span>
</button> </button>
<a class="navbar-brand" href="#"><img src="icon_inv.png" style="max-height: 26px; display: inline;"/> PiGallery2</a> <a class="navbar-brand" href="#"><img src="assets/icon_inv.png" style="max-height: 26px; display: inline;"/> PiGallery2</a>
</div> </div>
<div id="navbar" class="collapse navbar-collapse"> <div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
@@ -29,4 +29,4 @@
</div><!--/.nav-collapse --> </div><!--/.nav-collapse -->
</div> </div>
</nav> </nav>
<ng-content select="[body]"></ng-content> <ng-content select="[body]"></ng-content>

View File

@@ -5,25 +5,25 @@ import {UserDTO} from "../../../common/entities/UserDTO";
import {Config} from "../../../common/config/public/Config"; import {Config} from "../../../common/config/public/Config";
@Component({ @Component({
selector: 'app-frame', selector: 'app-frame',
templateUrl: 'app/frame/frame.component.html', templateUrl: './frame.component.html',
providers: [RouterLink], providers: [RouterLink],
encapsulation: ViewEncapsulation.Emulated encapsulation: ViewEncapsulation.Emulated
}) })
export class FrameComponent { export class FrameComponent {
user: UserDTO; user: UserDTO;
authenticationRequired:boolean = false; authenticationRequired: boolean = false;
constructor(private _authService:AuthenticationService) { constructor(private _authService: AuthenticationService) {
this.user = this._authService.getUser(); this.user = this._authService.getUser();
this.authenticationRequired = Config.Client.authenticationRequired; this.authenticationRequired = Config.Client.authenticationRequired;
} }
logout() { logout() {
this._authService.logout(); this._authService.logout();
} }
} }

View File

@@ -3,40 +3,40 @@ import {Utils} from "../../../common/Utils";
export class IconPhoto { export class IconPhoto {
protected replacementSizeCache: number|boolean = false; protected replacementSizeCache: number | boolean = false;
constructor(public photo: PhotoDTO) { constructor(public photo: PhotoDTO) {
}
iconLoaded() {
this.photo.readyIcon = true;
}
isIconAvailable() {
return this.photo.readyIcon;
}
getIconPath() {
return Utils.concatUrls("/api/gallery/content/", this.photo.directory.path, this.photo.directory.name, this.photo.name, "icon");
}
getPhotoPath() {
return Utils.concatUrls("/api/gallery/content/", this.photo.directory.path, this.photo.directory.name, this.photo.name);
}
equals(other: PhotoDTO | IconPhoto): boolean {
//is gridphoto
if (other instanceof IconPhoto) {
return this.photo.directory.path === other.photo.directory.path && this.photo.directory.name === other.photo.directory.name && this.photo.name === other.photo.name
} }
iconLoaded() { //is photo
this.photo.readyIcon = true; if (other.directory) {
return this.photo.directory.path === other.directory.path && this.photo.directory.name === other.directory.name && this.photo.name === other.name
} }
isIconAvailable() { return false;
return this.photo.readyIcon; }
} }
getIconPath() {
return Utils.concatUrls("/api/gallery/content/", this.photo.directory.path, this.photo.directory.name, this.photo.name, "icon");
}
getPhotoPath() {
return Utils.concatUrls("/api/gallery/content/", this.photo.directory.path, this.photo.directory.name, this.photo.name);
}
equals(other: PhotoDTO | IconPhoto): boolean {
//is gridphoto
if (other instanceof IconPhoto) {
return this.photo.directory.path === other.photo.directory.path && this.photo.directory.name === other.photo.directory.name && this.photo.name === other.photo.name
}
//is photo
if (other.directory) {
return this.photo.directory.path === other.directory.path && this.photo.directory.name === other.directory.name && this.photo.name === other.name
}
return false;
}
}

View File

@@ -5,59 +5,59 @@ import {Config} from "../../../common/config/public/Config";
export class Photo extends IconPhoto { export class Photo extends IconPhoto {
constructor(photo: PhotoDTO, public renderWidth: number, public renderHeight: number) { constructor(photo: PhotoDTO, public renderWidth: number, public renderHeight: number) {
super(photo); super(photo);
}
thumbnailLoaded() {
if (!this.isThumbnailAvailable()) {
this.photo.readyThumbnails = this.photo.readyThumbnails || [];
this.photo.readyThumbnails.push(this.getThumbnailSize());
} }
}
getThumbnailSize() {
let renderSize = Math.sqrt(this.renderWidth * this.renderHeight);
return Utils.findClosest(renderSize, Config.Client.thumbnailSizes);
}
thumbnailLoaded() { getReplacementThumbnailSize(): number {
if (!this.isThumbnailAvailable()) {
this.photo.readyThumbnails = this.photo.readyThumbnails || []; if (this.replacementSizeCache === false) {
this.photo.readyThumbnails.push(this.getThumbnailSize()); this.replacementSizeCache = null;
let size = this.getThumbnailSize();
if (!!this.photo.readyThumbnails) {
for (let i = 0; i < this.photo.readyThumbnails.length; i++) {
if (this.photo.readyThumbnails[i] < size) {
this.replacementSizeCache = this.photo.readyThumbnails[i];
break;
}
} }
}
} }
return <number>this.replacementSizeCache;
}
getThumbnailSize() { isReplacementThumbnailAvailable() {
let renderSize = Math.sqrt(this.renderWidth * this.renderHeight); return this.getReplacementThumbnailSize() !== null;
return Utils.findClosest(renderSize, Config.Client.thumbnailSizes); }
}
getReplacementThumbnailSize(): number { isThumbnailAvailable() {
return this.photo.readyThumbnails && this.photo.readyThumbnails.indexOf(this.getThumbnailSize()) != -1;
}
if (this.replacementSizeCache === false) { getReplacementThumbnailPath() {
this.replacementSizeCache = null; let size = this.getReplacementThumbnailSize();
return Utils.concatUrls("/api/gallery/content/", this.photo.directory.path, this.photo.directory.name, this.photo.name, "thumbnail", size.toString());
let size = this.getThumbnailSize(); }
if (!!this.photo.readyThumbnails) {
for (let i = 0; i < this.photo.readyThumbnails.length; i++) {
if (this.photo.readyThumbnails[i] < size) {
this.replacementSizeCache = this.photo.readyThumbnails[i];
break;
}
}
}
}
return <number>this.replacementSizeCache;
}
isReplacementThumbnailAvailable() { getThumbnailPath() {
return this.getReplacementThumbnailSize() !== null; let size = this.getThumbnailSize();
} return Utils.concatUrls("/api/gallery/content/", this.photo.directory.path, this.photo.directory.name, this.photo.name, "thumbnail", size.toString());
}
isThumbnailAvailable() {
return this.photo.readyThumbnails && this.photo.readyThumbnails.indexOf(this.getThumbnailSize()) != -1;
}
getReplacementThumbnailPath() {
let size = this.getReplacementThumbnailSize();
return Utils.concatUrls("/api/gallery/content/", this.photo.directory.path, this.photo.directory.name, this.photo.name, "thumbnail", size.toString());
}
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

@@ -8,79 +8,79 @@ import {Config} from "../../../common/config/public/Config";
export class GalleryCacheService { export class GalleryCacheService {
public getDirectory(directoryName: string): DirectoryDTO { public getDirectory(directoryName: string): DirectoryDTO {
if (Config.Client.enableCache == false) { if (Config.Client.enableCache == false) {
return null; return null;
}
let value = localStorage.getItem(directoryName);
if (value != null) {
let directory: DirectoryDTO = JSON.parse(value);
//Add references
let addDir = (dir: DirectoryDTO) => {
dir.photos.forEach((photo: PhotoDTO) => {
photo.directory = dir;
});
dir.directories.forEach((directory: DirectoryDTO) => {
addDir(directory);
directory.parent = dir;
});
};
addDir(directory);
return directory;
}
return null;
} }
let value = localStorage.getItem(directoryName);
if (value != null) {
let directory: DirectoryDTO = JSON.parse(value);
public setDirectory(directory: DirectoryDTO): void {
if (Config.Client.enableCache == false) {
return;
}
localStorage.setItem(Utils.concatUrls(directory.path, directory.name), JSON.stringify(directory)); //Add references
let addDir = (dir: DirectoryDTO) => {
directory.directories.forEach((dir: DirectoryDTO) => { dir.photos.forEach((photo: PhotoDTO) => {
let name = Utils.concatUrls(dir.path, dir.name); photo.directory = dir;
if (localStorage.getItem(name) == null) { //don't override existing
localStorage.setItem(Utils.concatUrls(dir.path, dir.name), JSON.stringify(dir));
}
}); });
dir.directories.forEach((directory: DirectoryDTO) => {
addDir(directory);
directory.parent = dir;
});
};
addDir(directory);
return directory;
}
return null;
}
public setDirectory(directory: DirectoryDTO): void {
if (Config.Client.enableCache == false) {
return;
} }
/** localStorage.setItem(Utils.concatUrls(directory.path, directory.name), JSON.stringify(directory));
* Update photo state at cache too (Eg.: thumbnail rendered)
* @param photo
*/
public photoUpdated(photo: PhotoDTO): void {
if (Config.Client.enableCache == false) { directory.directories.forEach((dir: DirectoryDTO) => {
return; let name = Utils.concatUrls(dir.path, dir.name);
} if (localStorage.getItem(name) == null) { //don't override existing
localStorage.setItem(Utils.concatUrls(dir.path, dir.name), JSON.stringify(dir));
}
});
let directoryName = Utils.concatUrls(photo.directory.path, photo.directory.name); }
let value = localStorage.getItem(directoryName);
if (value != null) {
let directory: DirectoryDTO = JSON.parse(value);
directory.photos.forEach((p) => {
if (p.name === photo.name) {
//update data
p.metadata = photo.metadata;
p.readyThumbnails = photo.readyThumbnails;
//save changes /**
localStorage.setItem(directoryName, JSON.stringify(directory)); * Update photo state at cache too (Eg.: thumbnail rendered)
return; * @param photo
} */
}); public photoUpdated(photo: PhotoDTO): void {
}
if (Config.Client.enableCache == false) {
return;
} }
let directoryName = Utils.concatUrls(photo.directory.path, photo.directory.name);
let value = localStorage.getItem(directoryName);
if (value != null) {
let directory: DirectoryDTO = JSON.parse(value);
directory.photos.forEach((p) => {
if (p.name === photo.name) {
//update data
p.metadata = photo.metadata;
p.readyThumbnails = photo.readyThumbnails;
//save changes
localStorage.setItem(directoryName, JSON.stringify(directory));
return;
}
});
}
}
} }

View File

@@ -4,10 +4,10 @@
<div class="photo-container"> <div class="photo-container">
<div class="photo" *ngIf="thumbnail && thumbnail.available" <div class="photo" *ngIf="thumbnail && thumbnail.Available"
[style.background-image]="'url('+thumbnail.src+')'"></div> [style.background-image]="'url('+thumbnail.src+')'"></div>
<span *ngIf="!thumbnail || !thumbnail.available" class="glyphicon glyphicon-folder-open no-image" <span *ngIf="!thumbnail || !thumbnail.Available" class="glyphicon glyphicon-folder-open no-image"
aria-hidden="true"> aria-hidden="true">
</span> </span>

View File

@@ -1,4 +1,4 @@
import {Component, Input, OnInit, OnDestroy, ViewChild, ElementRef} from "@angular/core"; import {Component, ElementRef, Input, OnDestroy, OnInit, ViewChild} from "@angular/core";
import {DirectoryDTO} from "../../../../common/entities/DirectoryDTO"; import {DirectoryDTO} from "../../../../common/entities/DirectoryDTO";
import {RouterLink} from "@angular/router"; import {RouterLink} from "@angular/router";
import {Utils} from "../../../../common/Utils"; import {Utils} from "../../../../common/Utils";
@@ -6,41 +6,41 @@ import {Photo} from "../Photo";
import {Thumbnail, ThumbnailManagerService} from "../thumnailManager.service"; import {Thumbnail, ThumbnailManagerService} from "../thumnailManager.service";
@Component({ @Component({
selector: 'gallery-directory', selector: 'gallery-directory',
templateUrl: 'app/gallery/directory/directory.gallery.component.html', templateUrl: './directory.gallery.component.html',
styleUrls: ['app/gallery/directory/directory.gallery.component.css'], styleUrls: ['./directory.gallery.component.css'],
providers: [RouterLink], providers: [RouterLink],
}) })
export class GalleryDirectoryComponent implements OnInit,OnDestroy { export class GalleryDirectoryComponent implements OnInit, OnDestroy {
@Input() directory: DirectoryDTO; @Input() directory: DirectoryDTO;
@ViewChild("dirContainer") container: ElementRef; @ViewChild("dirContainer") container: ElementRef;
thumbnail: Thumbnail = null; thumbnail: Thumbnail = null;
constructor(private thumbnailService: ThumbnailManagerService) {
}
ngOnInit() {
if (this.directory.photos.length > 0) {
this.thumbnail = this.thumbnailService.getThumbnail(new Photo(this.directory.photos[0], 100, 100));
constructor(private thumbnailService: ThumbnailManagerService) {
} }
}
ngOnInit() { //TODO: implement scroll
if (this.directory.photos.length > 0) { isInView(): boolean {
this.thumbnail = this.thumbnailService.getThumbnail(new Photo(this.directory.photos[0], 100, 100)); return document.body.scrollTop < this.container.nativeElement.offsetTop + this.container.nativeElement.clientHeight
&& document.body.scrollTop + window.innerHeight > this.container.nativeElement.offsetTop;
}
} getDirectoryPath() {
} return Utils.concatUrls(this.directory.path, this.directory.name);
}
//TODO: implement scroll
isInView(): boolean { ngOnDestroy() {
return document.body.scrollTop < this.container.nativeElement.offsetTop + this.container.nativeElement.clientHeight if (this.thumbnail != null) {
&& document.body.scrollTop + window.innerHeight > this.container.nativeElement.offsetTop; this.thumbnail.destroy();
}
getDirectoryPath() {
return Utils.concatUrls(this.directory.path, this.directory.name);
}
ngOnDestroy() {
if (this.thumbnail != null) {
this.thumbnail.destroy();
}
} }
}
} }

View File

@@ -5,42 +5,42 @@ import {Event} from "../../../common/event/Event";
export class FullScreenService { export class FullScreenService {
OnFullScreenChange = new Event<boolean>(); OnFullScreenChange = new Event<boolean>();
public isFullScreenEnabled(): boolean { public isFullScreenEnabled(): boolean {
return !!(document.fullscreenElement || document['mozFullScreenElement'] || document.webkitFullscreenElement); return !!(document.fullscreenElement || document['mozFullScreenElement'] || document.webkitFullscreenElement);
}
public showFullScreen(element: any) {
if (this.isFullScreenEnabled()) {
return;
} }
public showFullScreen(element: any) { if (element.requestFullscreen) {
if (this.isFullScreenEnabled()) { element.requestFullscreen();
return; } else if (element.mozRequestFullScreen) {
} element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
this.OnFullScreenChange.trigger(true);
}
if (element.requestFullscreen) { public exitFullScreen() {
element.requestFullscreen(); if (!this.isFullScreenEnabled()) {
} else if (element.mozRequestFullScreen) { return;
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
this.OnFullScreenChange.trigger(true);
} }
public exitFullScreen() { if (document.exitFullscreen) {
if (!this.isFullScreenEnabled()) { document.exitFullscreen();
return; } else if (document['mozCancelFullScreen']) {
} document['mozCancelFullScreen']();
} else if (document.webkitExitFullscreen) {
if (document.exitFullscreen) { document.webkitExitFullscreen();
document.exitFullscreen();
} else if (document['mozCancelFullScreen']) {
document['mozCancelFullScreen']();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
this.OnFullScreenChange.trigger(false);
} }
this.OnFullScreenChange.trigger(false);
}
} }

View File

@@ -8,65 +8,63 @@ import {SearchTypes} from "../../../common/entities/AutoCompleteItem";
import {Config} from "../../../common/config/public/Config"; import {Config} from "../../../common/config/public/Config";
@Component({ @Component({
selector: 'gallery', selector: 'gallery',
templateUrl: 'app/gallery/gallery.component.html', templateUrl: './gallery.component.html',
styleUrls: ['app/gallery/gallery.component.css'] styleUrls: ['./gallery.component.css']
}) })
export class GalleryComponent implements OnInit { export class GalleryComponent implements OnInit {
@ViewChild(GallerySearchComponent) search: GallerySearchComponent; @ViewChild(GallerySearchComponent) search: GallerySearchComponent;
@ViewChild(GalleryGridComponent) grid: GalleryGridComponent; @ViewChild(GalleryGridComponent) grid: GalleryGridComponent;
public showSearchBar: boolean = true; public showSearchBar: boolean = true;
constructor(private _galleryService: GalleryService, constructor(public _galleryService: GalleryService,
private _authService: AuthenticationService, private _authService: AuthenticationService,
private _router: Router, private _router: Router,
private _route: ActivatedRoute) { private _route: ActivatedRoute) {
this.showSearchBar = Config.Client.Search.searchEnabled; this.showSearchBar = Config.Client.Search.searchEnabled;
}
ngOnInit() {
if (!this._authService.isAuthenticated()) {
this._router.navigate(['login']);
return;
} }
ngOnInit() { this._route.params
if (!this._authService.isAuthenticated()) { .subscribe((params: Params) => {
this._router.navigate(['login']); let searchText = params['searchText'];
if (searchText && searchText != "") {
console.log("searching");
let typeString = params['type'];
if (typeString && typeString != "") {
console.log("with type");
let type: SearchTypes = <any>SearchTypes[typeString];
this._galleryService.search(searchText, type);
return; return;
}
this._galleryService.search(searchText);
return;
} }
this._route.params
.subscribe((params: Params) => {
let searchText = params['searchText'];
if (searchText && searchText != "") {
console.log("searching");
let typeString = params['type'];
if (typeString && typeString != "") { let directoryName = params['directory'];
console.log("with type"); directoryName = directoryName ? directoryName : "";
let type: SearchTypes = <any>SearchTypes[typeString];
this._galleryService.search(searchText, type);
return;
}
this._galleryService.search(searchText); this._galleryService.getDirectory(directoryName);
return;
} });
let directoryName = params['directory']; }
directoryName = directoryName ? directoryName : "";
this._galleryService.getDirectory(directoryName); onLightboxLastElement() {
this.grid.renderARow();
}); }
}
onLightboxLastElement() {
this.grid.renderARow();
}
} }

View File

@@ -10,104 +10,104 @@ import {GalleryCacheService} from "./cache.gallery.service";
@Injectable() @Injectable()
export class GalleryService { export class GalleryService {
public content: ContentWrapper; public content: ContentWrapper;
private lastDirectory: DirectoryDTO; private lastDirectory: DirectoryDTO;
private searchId: any; private searchId: any;
constructor(private networkService: NetworkService, private galleryCacheService: GalleryCacheService) { constructor(private networkService: NetworkService, private galleryCacheService: GalleryCacheService) {
this.content = new ContentWrapper(); this.content = new ContentWrapper();
} }
lastRequest: {directory: string} = { lastRequest: { directory: string } = {
directory: null directory: null
}; };
public getDirectory(directoryName: string): Promise<Message<ContentWrapper>> { public getDirectory(directoryName: string): Promise<Message<ContentWrapper>> {
this.content = new ContentWrapper(); this.content = new ContentWrapper();
this.content.directory = this.galleryCacheService.getDirectory(directoryName); this.content.directory = this.galleryCacheService.getDirectory(directoryName);
this.content.searchResult = null; this.content.searchResult = null;
this.lastRequest.directory = directoryName; this.lastRequest.directory = directoryName;
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) {
this.galleryCacheService.setDirectory(message.result.directory); //save it before adding references this.galleryCacheService.setDirectory(message.result.directory); //save it before adding references
if (this.lastRequest.directory != directoryName) { if (this.lastRequest.directory != directoryName) {
return; return;
} }
//Add references //Add references
let addDir = (dir: DirectoryDTO) => { let addDir = (dir: DirectoryDTO) => {
dir.photos.forEach((photo: PhotoDTO) => { dir.photos.forEach((photo: PhotoDTO) => {
photo.directory = dir; photo.directory = dir;
});
dir.directories.forEach((directory: DirectoryDTO) => {
addDir(directory);
directory.parent = dir;
});
};
addDir(message.result.directory);
this.lastDirectory = message.result.directory;
this.content = message.result;
}
return message;
});
}
//TODO: cache
public search(text: string, type?: SearchTypes): Promise<Message<ContentWrapper>> {
clearTimeout(this.searchId);
if (text === null || text === '') {
return Promise.resolve(new Message(null, null));
}
let queryString = "/search/" + text;
if (type) {
queryString += "?type=" + type;
}
return this.networkService.getJson(queryString).then(
(message: Message<ContentWrapper>) => {
if (!message.error && message.result) {
this.content = message.result;
}
return message;
});
}
//TODO: cache (together with normal search)
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("/instant-search/" + text).then(
(message: Message<ContentWrapper>) => {
if (!message.error && message.result) {
this.content = message.result;
}
return message;
}); });
dir.directories.forEach((directory: DirectoryDTO) => {
addDir(directory);
directory.parent = dir;
});
};
addDir(message.result.directory);
this.lastDirectory = message.result.directory;
this.content = message.result;
}
return message;
});
}
//TODO: cache
public search(text: string, type?: SearchTypes): Promise<Message<ContentWrapper>> {
clearTimeout(this.searchId);
if (text === null || text === '') {
return Promise.resolve(new Message(null, null));
} }
let queryString = "/search/" + text;
if (type) {
queryString += "?type=" + type;
}
return this.networkService.getJson(queryString).then(
(message: Message<ContentWrapper>) => {
if (!message.error && message.result) {
this.content = message.result;
}
return message;
});
}
//TODO: cache (together with normal search)
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("/instant-search/" + text).then(
(message: Message<ContentWrapper>) => {
if (!message.error && message.result) {
this.content = message.result;
}
return message;
});
}
} }

View File

@@ -3,9 +3,9 @@ import {Photo} from "../Photo";
export class GridPhoto extends Photo { export class GridPhoto extends Photo {
constructor(photo: PhotoDTO, renderWidth: number, renderHeight: number, public rowId: number) { constructor(photo: PhotoDTO, renderWidth: number, renderHeight: number, public rowId: number) {
super(photo, renderWidth, renderHeight); super(photo, renderWidth, renderHeight);
} }
} }

View File

@@ -2,63 +2,63 @@ import {PhotoDTO} from "../../../../common/entities/PhotoDTO";
export class GridRowBuilder { export class GridRowBuilder {
private photoRow: Array<PhotoDTO> = []; private photoRow: Array<PhotoDTO> = [];
private photoIndex:number = 0; //index of the last pushed photo to the photoRow private photoIndex: number = 0; //index of the last pushed photo to the photoRow
constructor(private photos: Array<PhotoDTO>, private startIndex: number, private photoMargin: number, private containerWidth: number) { constructor(private photos: Array<PhotoDTO>, private startIndex: number, private photoMargin: number, private containerWidth: number) {
this.photoIndex = startIndex; this.photoIndex = startIndex;
}
public addPhotos(number: number) {
for (let i = 0; i < number; i++) {
this.addPhoto();
}
}
public addPhoto(): boolean {
if (this.photoIndex + 1 > this.photos.length) {
return false;
}
this.photoRow.push(this.photos[this.photoIndex]);
this.photoIndex++;
return true;
}
public removePhoto(): boolean {
if (this.photoIndex - 1 < this.startIndex) {
return false;
}
this.photoIndex--;
this.photoRow.pop();
return true;
}
public getPhotoRow(): Array<PhotoDTO> {
return this.photoRow;
}
public adjustRowHeightBetween(minHeight: number, maxHeight: number) {
while (this.calcRowHeight() > maxHeight && this.addPhoto() === true) { //row too high -> add more images
} }
public addPhotos(number:number) { while (this.calcRowHeight() < minHeight && this.removePhoto() === true) { //roo too small -> remove images
for (let i = 0; i < number; i++) {
this.addPhoto();
}
} }
public addPhoto():boolean { //keep at least one photo int thr row
if (this.photoIndex + 1 > this.photos.length) { if (this.photoRow.length <= 0) {
return false; this.addPhoto();
}
this.photoRow.push(this.photos[this.photoIndex]);
this.photoIndex++;
return true;
} }
}
public removePhoto():boolean { public calcRowHeight(): number {
if (this.photoIndex - 1 < this.startIndex) { let width = 0;
return false; for (let i = 0; i < this.photoRow.length; i++) {
} width += ((this.photoRow[i].metadata.size.width) / (this.photoRow[i].metadata.size.height)); //summing up aspect ratios
this.photoIndex--;
this.photoRow.pop();
return true;
} }
let height = (this.containerWidth - this.photoRow.length * (this.photoMargin * 2) - 1) / width; //cant be equal -> width-1
public getPhotoRow(): Array<PhotoDTO> { return height + (this.photoMargin * 2);
return this.photoRow; };
} }
public adjustRowHeightBetween(minHeight:number, maxHeight:number) {
while (this.calcRowHeight() > maxHeight && this.addPhoto() === true) { //row too high -> add more images
}
while (this.calcRowHeight() < minHeight && this.removePhoto() === true) { //roo too small -> remove images
}
//keep at least one photo int thr row
if (this.photoRow.length <= 0) {
this.addPhoto();
}
}
public calcRowHeight():number {
let width = 0;
for (let i = 0; i < this.photoRow.length; i++) {
width += ((this.photoRow[i].metadata.size.width) / (this.photoRow[i].metadata.size.height)); //summing up aspect ratios
}
let height = (this.containerWidth - this.photoRow.length * (this.photoMargin * 2) - 1) / width; //cant be equal -> width-1
return height + (this.photoMargin * 2);
};
}

View File

@@ -1,14 +1,14 @@
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
HostListener, HostListener,
Input, Input,
OnChanges, OnChanges,
QueryList, QueryList,
ViewChild, ViewChild,
ViewChildren ViewChildren
} from "@angular/core"; } from "@angular/core";
import {PhotoDTO} from "../../../../common/entities/PhotoDTO"; import {PhotoDTO} from "../../../../common/entities/PhotoDTO";
import {GridRowBuilder} from "./GridRowBuilder"; import {GridRowBuilder} from "./GridRowBuilder";
@@ -19,197 +19,197 @@ import {OverlayService} from "../overlay.service";
import {Config} from "../../../../common/config/public/Config"; import {Config} from "../../../../common/config/public/Config";
@Component({ @Component({
selector: 'gallery-grid', selector: 'gallery-grid',
templateUrl: 'app/gallery/grid/grid.gallery.component.html', templateUrl: './grid.gallery.component.html',
styleUrls: ['app/gallery/grid/grid.gallery.component.css'], styleUrls: ['./grid.gallery.component.css'],
}) })
export class GalleryGridComponent implements OnChanges, AfterViewInit { 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() photos: Array<PhotoDTO>; @Input() photos: Array<PhotoDTO>;
@Input() lightbox: GalleryLightboxComponent; @Input() lightbox: GalleryLightboxComponent;
photosToRender: Array<GridPhoto> = []; photosToRender: Array<GridPhoto> = [];
containerWidth: number = 0; containerWidth: number = 0;
private IMAGE_MARGIN = 2; private IMAGE_MARGIN = 2;
private TARGET_COL_COUNT = 5; private TARGET_COL_COUNT = 5;
private MIN_ROW_COUNT = 2; private MIN_ROW_COUNT = 2;
private MAX_ROW_COUNT = 5; private MAX_ROW_COUNT = 5;
private onScrollFired = false; private onScrollFired = false;
private scrollbarWidth = 0; private scrollbarWidth = 0;
constructor(private overlayService: OverlayService, private changeDetector: ChangeDetectorRef) { constructor(private overlayService: OverlayService, private changeDetector: ChangeDetectorRef) {
}
ngOnChanges() {
if (this.isAfterViewInit === false) {
return;
}
this.updateContainerWidth();
this.sortPhotos();
this.mergeNewPhotos();
setTimeout(() => {
this.renderPhotos();
}, 0);
}
@HostListener('window:resize')
onResize() {
if (this.isAfterViewInit === false) {
return;
}
this.updateContainerWidth();
this.sortPhotos();
//render the same amount of images on resize
let renderedIndex = this.renderedPhotoIndex;
this.clearRenderedPhotos();
this.renderPhotos(renderedIndex);
}
isAfterViewInit: boolean = false;
ngAfterViewInit() {
this.lightbox.setGridPhotoQL(this.gridPhotoQL);
//TODO: implement scroll detection
this.updateContainerWidth();
this.sortPhotos();
this.clearRenderedPhotos();
setTimeout(() => {
this.renderPhotos();
}, 0);
this.isAfterViewInit = true;
}
private sortPhotos() {
//sort pohots by date
this.photos.sort((a: PhotoDTO, b: PhotoDTO) => {
return a.metadata.creationDate - b.metadata.creationDate;
});
}
private clearRenderedPhotos() {
this.photosToRender = [];
this.renderedPhotoIndex = 0;
this.changeDetector.detectChanges();
}
private mergeNewPhotos() {
//merge new data with old one
let lastSameIndex = 0;
let lastRowId = null;
for (let i = 0; i < this.photos.length && i < this.photosToRender.length; i++) {
//thIf a photo changed the whole row has to be removed
if (this.photosToRender[i].rowId != lastRowId) {
lastSameIndex = i;
lastRowId = this.photosToRender[i].rowId;
}
if (this.photosToRender[i].equals(this.photos[i]) === false) {
break;
}
} }
ngOnChanges() { if (lastSameIndex > 0) {
if (this.isAfterViewInit === false) { this.photosToRender.splice(lastSameIndex, this.photosToRender.length - lastSameIndex);
return; this.renderedPhotoIndex = lastSameIndex;
} else {
this.clearRenderedPhotos();
}
}
private renderedPhotoIndex: number = 0;
private renderPhotos(numberOfPhotos: number = 0) {
if (this.containerWidth == 0 || this.renderedPhotoIndex >= this.photos.length || !this.shouldRenderMore()) {
return;
}
let renderedContentHeight = 0;
while (this.renderedPhotoIndex < this.photos.length && (this.shouldRenderMore(renderedContentHeight) === true || this.renderedPhotoIndex < numberOfPhotos)) {
let ret = this.renderARow();
if (ret === null) {
throw new Error("Gridphotos rendering failed");
}
renderedContentHeight += ret;
}
}
/**
* Returns true, if scroll is >= 70% to render more images.
* Or of onscroll renderin is off: return always to render all the images at once
* @param offset Add height to the client height (conent is not yet added to the dom, but calculate with it)
* @returns {boolean}
*/
private shouldRenderMore(offset: number = 0): boolean {
return Config.Client.enableOnScrollRendering === false ||
window.scrollY >= (document.body.clientHeight + offset - window.innerHeight) * 0.7
|| (document.body.clientHeight + offset) * 0.85 < window.innerHeight;
}
@HostListener('window:scroll')
onScroll() {
if (!this.onScrollFired) {
window.requestAnimationFrame(() => {
this.renderPhotos();
if (Config.Client.enableOnScrollThumbnailPrioritising === true) {
this.gridPhotoQL.toArray().forEach((pc: GalleryPhotoComponent) => {
pc.onScroll();
});
} }
this.updateContainerWidth(); this.onScrollFired = false;
this.sortPhotos(); });
this.mergeNewPhotos(); this.onScrollFired = true;
setImmediate(() => {
this.renderPhotos();
});
} }
}
@HostListener('window:resize') public renderARow(): number {
onResize() { if (this.renderedPhotoIndex >= this.photos.length) {
if (this.isAfterViewInit === false) { return null;
return;
}
this.updateContainerWidth();
this.sortPhotos();
//render the same amount of images on resize
let renderedIndex = this.renderedPhotoIndex;
this.clearRenderedPhotos();
this.renderPhotos(renderedIndex);
} }
isAfterViewInit: boolean = false; let maxRowHeight = window.innerHeight / this.MIN_ROW_COUNT;
let minRowHeight = window.innerHeight / this.MAX_ROW_COUNT;
ngAfterViewInit() { let photoRowBuilder = new GridRowBuilder(this.photos, this.renderedPhotoIndex, this.IMAGE_MARGIN, this.containerWidth - this.overlayService.getPhantomScrollbarWidth());
this.lightbox.setGridPhotoQL(this.gridPhotoQL); photoRowBuilder.addPhotos(this.TARGET_COL_COUNT);
photoRowBuilder.adjustRowHeightBetween(minRowHeight, maxRowHeight);
//TODO: implement scroll detection let rowHeight = photoRowBuilder.calcRowHeight();
let imageHeight = rowHeight - (this.IMAGE_MARGIN * 2);
photoRowBuilder.getPhotoRow().forEach((photo) => {
let imageWidth = imageHeight * (photo.metadata.size.width / photo.metadata.size.height);
this.photosToRender.push(new GridPhoto(photo, imageWidth, imageHeight, this.renderedPhotoIndex));
});
this.updateContainerWidth(); this.renderedPhotoIndex += photoRowBuilder.getPhotoRow().length;
this.sortPhotos(); return rowHeight;
this.clearRenderedPhotos(); }
setImmediate(() => {
this.renderPhotos(); private updateContainerWidth(): number {
}); if (!this.gridContainer) {
this.isAfterViewInit = true; return;
}
private sortPhotos() {
//sort pohots by date
this.photos.sort((a: PhotoDTO, b: PhotoDTO) => {
return a.metadata.creationDate - b.metadata.creationDate;
});
}
private clearRenderedPhotos() {
this.photosToRender = [];
this.renderedPhotoIndex = 0;
this.changeDetector.detectChanges();
}
private mergeNewPhotos() {
//merge new data with old one
let lastSameIndex = 0;
let lastRowId = null;
for (let i = 0; i < this.photos.length && i < this.photosToRender.length; i++) {
//thIf a photo changed the whole row has to be removed
if (this.photosToRender[i].rowId != lastRowId) {
lastSameIndex = i;
lastRowId = this.photosToRender[i].rowId;
}
if (this.photosToRender[i].equals(this.photos[i]) === false) {
break;
}
}
if (lastSameIndex > 0) {
this.photosToRender.splice(lastSameIndex, this.photosToRender.length - lastSameIndex);
this.renderedPhotoIndex = lastSameIndex;
} else {
this.clearRenderedPhotos();
}
}
private renderedPhotoIndex: number = 0;
private renderPhotos(numberOfPhotos: number = 0) {
if (this.containerWidth == 0 || this.renderedPhotoIndex >= this.photos.length || !this.shouldRenderMore()) {
return;
}
let renderedContentHeight = 0;
while (this.renderedPhotoIndex < this.photos.length && (this.shouldRenderMore(renderedContentHeight) === true || this.renderedPhotoIndex < numberOfPhotos)) {
let ret = this.renderARow();
if (ret === null) {
throw new Error("Gridphotos rendering failed");
}
renderedContentHeight += ret;
}
}
/**
* Returns true, if scroll is >= 70% to render more images.
* Or of onscroll renderin is off: return always to render all the images at once
* @param offset Add height to the client height (conent is not yet added to the dom, but calculate with it)
* @returns {boolean}
*/
private shouldRenderMore(offset: number = 0): boolean {
return Config.Client.enableOnScrollRendering === false ||
window.scrollY >= (document.body.clientHeight + offset - window.innerHeight) * 0.7
|| (document.body.clientHeight + offset) * 0.85 < window.innerHeight;
}
@HostListener('window:scroll')
onScroll() {
if (!this.onScrollFired) {
window.requestAnimationFrame(() => {
this.renderPhotos();
if (Config.Client.enableOnScrollThumbnailPrioritising === true) {
this.gridPhotoQL.toArray().forEach((pc: GalleryPhotoComponent) => {
pc.onScroll();
});
}
this.onScrollFired = false;
});
this.onScrollFired = true;
}
}
public renderARow(): number {
if (this.renderedPhotoIndex >= this.photos.length) {
return null;
}
let maxRowHeight = window.innerHeight / this.MIN_ROW_COUNT;
let minRowHeight = window.innerHeight / this.MAX_ROW_COUNT;
let photoRowBuilder = new GridRowBuilder(this.photos, this.renderedPhotoIndex, this.IMAGE_MARGIN, this.containerWidth - this.overlayService.getPhantomScrollbarWidth());
photoRowBuilder.addPhotos(this.TARGET_COL_COUNT);
photoRowBuilder.adjustRowHeightBetween(minRowHeight, maxRowHeight);
let rowHeight = photoRowBuilder.calcRowHeight();
let imageHeight = rowHeight - (this.IMAGE_MARGIN * 2);
photoRowBuilder.getPhotoRow().forEach((photo) => {
let imageWidth = imageHeight * (photo.metadata.size.width / photo.metadata.size.height);
this.photosToRender.push(new GridPhoto(photo, imageWidth, imageHeight, this.renderedPhotoIndex));
});
this.renderedPhotoIndex += photoRowBuilder.getPhotoRow().length;
return rowHeight;
}
private updateContainerWidth(): number {
if (!this.gridContainer) {
return;
}
this.containerWidth = this.gridContainer.nativeElement.clientWidth;
} }
this.containerWidth = this.gridContainer.nativeElement.clientWidth;
}
} }

View File

@@ -1,13 +1,13 @@
import {Component, Input} from "@angular/core"; import {Component, Input} from "@angular/core";
@Component({ @Component({
selector: 'gallery-grid-photo-loading', selector: 'gallery-grid-photo-loading',
templateUrl: 'app/gallery/grid/photo/loading/loading.photo.grid.gallery.component.html', templateUrl: './loading.photo.grid.gallery.component.html',
styleUrls: ['app/gallery/grid/photo/loading/loading.photo.grid.gallery.component.css'], styleUrls: ['./loading.photo.grid.gallery.component.css'],
}) })
export class GalleryPhotoLoadingComponent { export class GalleryPhotoLoadingComponent {
@Input() animate:boolean; @Input() animate: boolean;
} }

View File

@@ -1,7 +1,7 @@
<div #photoContainer class="photo-container" (mouseover)="hover()" (mouseout)="mouseOut()"> <div #photoContainer class="photo-container" (mouseover)="hover()" (mouseout)="mouseOut()">
<img #img [src]="thumbnail.src" [hidden]="!thumbnail.available"> <img #img [src]="thumbnail.Src" [hidden]="!thumbnail.Available">
<gallery-grid-photo-loading [animate]="thumbnail.loading" *ngIf="!thumbnail.available"> <gallery-grid-photo-loading [animate]="thumbnail.loading" *ngIf="!thumbnail.Available">
</gallery-grid-photo-loading> </gallery-grid-photo-loading>
<!--Info box --> <!--Info box -->
@@ -30,4 +30,4 @@
</div> </div>
</div> </div>
</div> </div>

View File

@@ -7,152 +7,152 @@ import {Thumbnail, ThumbnailManagerService} from "../../thumnailManager.service"
import {Config} from "../../../../../common/config/public/Config"; import {Config} from "../../../../../common/config/public/Config";
@Component({ @Component({
selector: 'gallery-grid-photo', selector: 'gallery-grid-photo',
templateUrl: 'app/gallery/grid/photo/photo.grid.gallery.component.html', templateUrl: './photo.grid.gallery.component.html',
styleUrls: ['app/gallery/grid/photo/photo.grid.gallery.component.css'], styleUrls: ['./photo.grid.gallery.component.css'],
providers: [RouterLink], providers: [RouterLink],
}) })
export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy { export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy {
@Input() gridPhoto: GridPhoto; @Input() gridPhoto: GridPhoto;
@ViewChild("img") imageRef: ElementRef; @ViewChild("img") imageRef: ElementRef;
@ViewChild("info") infoDiv: ElementRef; @ViewChild("info") infoDiv: ElementRef;
@ViewChild("photoContainer") container: ElementRef; @ViewChild("photoContainer") container: ElementRef;
thumbnail: Thumbnail; thumbnail: Thumbnail;
/* /*
image = { image = {
src: '', src: '',
show: false show: false
}; };
loading = { loading = {
animate: false, animate: false,
show: true show: true
}; };
*/ */
infoStyle = { infoStyle = {
height: 0, height: 0,
background: "rgba(0,0,0,0.0)" background: "rgba(0,0,0,0.0)"
}; };
SearchTypes: any = []; SearchTypes: any = [];
searchEnabled: boolean = true; searchEnabled: boolean = true;
wasInView: boolean = null; wasInView: boolean = null;
constructor(private thumbnailService: ThumbnailManagerService) { constructor(private thumbnailService: ThumbnailManagerService) {
this.SearchTypes = SearchTypes; this.SearchTypes = SearchTypes;
this.searchEnabled = Config.Client.Search.searchEnabled; this.searchEnabled = Config.Client.Search.searchEnabled;
} }
ngOnInit() { ngOnInit() {
this.thumbnail = this.thumbnailService.getThumbnail(this.gridPhoto); this.thumbnail = this.thumbnailService.getThumbnail(this.gridPhoto);
/* this.loading.show = true; /* this.loading.show = true;
//set up before adding task to thumbnail generator //set up before adding task to thumbnail generator
if (this.gridPhoto.isThumbnailAvailable()) { if (this.gridPhoto.isThumbnailAvailable()) {
this.image.src = this.gridPhoto.getThumbnailPath();
this.image.show = true;
} else if (this.gridPhoto.isReplacementThumbnailAvailable()) {
this.image.src = this.gridPhoto.getReplacementThumbnailPath();
this.image.show = true;
}*/
}
/*
ngAfterViewInit() {
//schedule change after Angular checks the model
if (!this.gridPhoto.isThumbnailAvailable()) {
setImmediate(() => {
let listener: ThumbnailLoadingListener = {
onStartedLoading: () => { //onLoadStarted
this.loading.animate = true;
},
onLoad: () => {//onLoaded
this.image.src = this.gridPhoto.getThumbnailPath(); this.image.src = this.gridPhoto.getThumbnailPath();
this.image.show = true; this.image.show = true;
this.loading.show = false; } else if (this.gridPhoto.isReplacementThumbnailAvailable()) {
this.thumbnailTask = null; this.image.src = this.gridPhoto.getReplacementThumbnailPath();
}, this.image.show = true;
onError: (error) => {//onError
this.thumbnailTask = null;
//TODO: handle error
//TODO: not an error if its from cache
console.error("something bad happened");
console.error(error);
}
};
if (this.gridPhoto.isReplacementThumbnailAvailable()) {
this.thumbnailTask = this.thumbnailService.loadImage(this.gridPhoto, ThumbnailLoadingPriority.medium, listener);
} else {
this.thumbnailTask = this.thumbnailService.loadImage(this.gridPhoto, ThumbnailLoadingPriority.high, listener);
}
});
}
}*/ }*/
ngOnDestroy() { }
this.thumbnail.destroy();
/* /*
if (this.thumbnailTask != null) { ngAfterViewInit() {
this.thumbnailService.removeTask(this.thumbnailTask); //schedule change after Angular checks the model
this.thumbnailTask = null; if (!this.gridPhoto.isThumbnailAvailable()) {
}*/ setImmediate(() => {
}
let listener: ThumbnailLoadingListener = {
onStartedLoading: () => { //onLoadStarted
this.loading.animate = true;
},
onLoad: () => {//onLoaded
this.image.src = this.gridPhoto.getThumbnailPath();
this.image.show = true;
this.loading.show = false;
this.thumbnailTask = null;
},
onError: (error) => {//onError
this.thumbnailTask = null;
//TODO: handle error
//TODO: not an error if its from cache
console.error("something bad happened");
console.error(error);
}
};
if (this.gridPhoto.isReplacementThumbnailAvailable()) {
this.thumbnailTask = this.thumbnailService.loadImage(this.gridPhoto, ThumbnailLoadingPriority.medium, listener);
} else {
this.thumbnailTask = this.thumbnailService.loadImage(this.gridPhoto, ThumbnailLoadingPriority.high, listener);
}
isInView(): boolean { });
return document.body.scrollTop < this.container.nativeElement.offsetTop + this.container.nativeElement.clientHeight }
&& document.body.scrollTop + window.innerHeight > this.container.nativeElement.offsetTop; }*/
}
onScroll() {
let isInView = this.isInView();
if (this.wasInView != isInView) {
this.wasInView = isInView;
this.thumbnail.Visible = isInView;
}
}
getPositionText(): string {
if (!this.gridPhoto) {
return ""
}
return this.gridPhoto.photo.metadata.positionData.city ||
this.gridPhoto.photo.metadata.positionData.state ||
this.gridPhoto.photo.metadata.positionData.country;
}
hover() {
this.infoStyle.height = this.infoDiv.nativeElement.clientHeight;
this.infoStyle.background = "rgba(0,0,0,0.8)";
}
mouseOut() {
this.infoStyle.height = 0;
this.infoStyle.background = "rgba(0,0,0,0.0)";
}
ngOnDestroy() {
this.thumbnail.destroy();
/* /*
onImageLoad() { if (this.thumbnailTask != null) {
this.loading.show = false; this.thumbnailService.removeTask(this.thumbnailTask);
} this.thumbnailTask = null;
*/ }*/
public getDimension(): Dimension { }
return <Dimension>{
top: this.imageRef.nativeElement.offsetTop,
left: this.imageRef.nativeElement.offsetLeft, isInView(): boolean {
width: this.imageRef.nativeElement.width, return document.body.scrollTop < this.container.nativeElement.offsetTop + this.container.nativeElement.clientHeight
height: this.imageRef.nativeElement.height && document.body.scrollTop + window.innerHeight > this.container.nativeElement.offsetTop;
}; }
onScroll() {
let isInView = this.isInView();
if (this.wasInView != isInView) {
this.wasInView = isInView;
this.thumbnail.Visible = isInView;
} }
}
getPositionText(): string {
if (!this.gridPhoto) {
return ""
}
return this.gridPhoto.photo.metadata.positionData.city ||
this.gridPhoto.photo.metadata.positionData.state ||
this.gridPhoto.photo.metadata.positionData.country;
}
hover() {
this.infoStyle.height = this.infoDiv.nativeElement.clientHeight;
this.infoStyle.background = "rgba(0,0,0,0.8)";
}
mouseOut() {
this.infoStyle.height = 0;
this.infoStyle.background = "rgba(0,0,0,0.0)";
}
/*
onImageLoad() {
this.loading.show = false;
}
*/
public getDimension(): Dimension {
return <Dimension>{
top: this.imageRef.nativeElement.offsetTop,
left: this.imageRef.nativeElement.offsetLeft,
width: this.imageRef.nativeElement.width,
height: this.imageRef.nativeElement.height
};
}
} }

View File

@@ -1,12 +1,12 @@
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
ElementRef, ElementRef,
EventEmitter, EventEmitter,
HostListener, HostListener,
Output, Output,
QueryList, QueryList,
ViewChild ViewChild
} from "@angular/core"; } from "@angular/core";
import {PhotoDTO} from "../../../../common/entities/PhotoDTO"; import {PhotoDTO} from "../../../../common/entities/PhotoDTO";
import {GalleryPhotoComponent} from "../grid/photo/photo.grid.gallery.component"; import {GalleryPhotoComponent} from "../grid/photo/photo.grid.gallery.component";
@@ -16,226 +16,226 @@ import {OverlayService} from "../overlay.service";
import {Subscription} from "rxjs"; import {Subscription} from "rxjs";
@Component({ @Component({
selector: 'gallery-lightbox', selector: 'gallery-lightbox',
styleUrls: ['app/gallery/lightbox/lightbox.gallery.component.css'], styleUrls: ['./lightbox.gallery.component.css'],
templateUrl: 'app/gallery/lightbox/lightbox.gallery.component.html', templateUrl: './lightbox.gallery.component.html',
}) })
export class GalleryLightboxComponent { export class GalleryLightboxComponent {
@Output('onLastElement') onLastElement = new EventEmitter(); @Output('onLastElement') onLastElement = new EventEmitter();
public navigation = {hasPrev: true, hasNext: true}; public navigation = {hasPrev: true, hasNext: true};
public photoDimension: Dimension = <Dimension>{top: 0, left: 0, width: 0, height: 0}; public photoDimension: Dimension = <Dimension>{top: 0, left: 0, width: 0, height: 0};
public lightboxDimension: Dimension = <Dimension>{top: 0, left: 0, width: 0, height: 0}; public lightboxDimension: Dimension = <Dimension>{top: 0, left: 0, width: 0, height: 0};
private transition: string = ""; public transition: string = "";
public blackCanvasOpacity: any = 0; public blackCanvasOpacity: any = 0;
private activePhoto: GalleryPhotoComponent; public activePhoto: GalleryPhotoComponent;
private gridPhotoQL: QueryList<GalleryPhotoComponent>; private gridPhotoQL: QueryList<GalleryPhotoComponent>;
private visible = false; public visible = false;
private changeSubscription: Subscription = null; private changeSubscription: Subscription = null;
@ViewChild("root") elementRef: ElementRef; @ViewChild("root") elementRef: ElementRef;
constructor(private fullScreenService: FullScreenService, private changeDetector: ChangeDetectorRef, private overlayService: OverlayService) { constructor(public fullScreenService: FullScreenService, private changeDetector: ChangeDetectorRef, private overlayService: OverlayService) {
}
//noinspection JSUnusedGlobalSymbols
@HostListener('window:resize', ['$event'])
onResize() {
if (this.activePhoto) {
this.disableAnimation();
this.lightboxDimension.width = this.getScreenWidth();
this.lightboxDimension.height = this.getScreenHeight();
this.updateActivePhoto(this.activePhotoId);
}
}
public nextImage() {
this.disableAnimation();
if (this.activePhotoId + 1 < this.gridPhotoQL.length) {
this.showPhoto(this.activePhotoId + 1);
if (this.activePhotoId + 3 >= this.gridPhotoQL.length) {
this.onLastElement.emit({}); //trigger to render more photos if there are
}
return;
}
console.warn("can't find photo to show next");
}
public prevImage() {
this.disableAnimation();
if (this.activePhotoId > 0) {
this.showPhoto(this.activePhotoId - 1);
return;
}
console.warn("can't find photo to show prev");
}
activePhotoId: number = null;
private showPhoto(photoIndex: number) {
this.activePhoto = null;
this.changeDetector.detectChanges();
this.updateActivePhoto(photoIndex);
}
private updateActivePhoto(photoIndex: number) {
let pcList = this.gridPhotoQL.toArray();
if (photoIndex < 0 || photoIndex > this.gridPhotoQL.length) {
throw new Error("Can't find the photo");
}
this.activePhotoId = photoIndex;
this.activePhoto = pcList[photoIndex];
this.photoDimension = this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.photo);
this.navigation.hasPrev = photoIndex > 0;
this.navigation.hasNext = photoIndex + 1 < pcList.length;
let to = this.activePhoto.getDimension();
//if target image out of screen -> scroll to there
if (this.getBodyScrollTop() > to.top || this.getBodyScrollTop() + this.getScreenHeight() < to.top) {
this.setBodyScrollTop(to.top);
} }
}
//noinspection JSUnusedGlobalSymbols public show(photo: PhotoDTO) {
@HostListener('window:resize', ['$event']) this.enableAnimation();
onResize() { this.visible = true;
if (this.activePhoto) { let selectedPhoto = this.findPhotoComponent(photo);
this.disableAnimation(); if (selectedPhoto === null) {
this.lightboxDimension.width = this.getScreenWidth(); throw new Error("Can't find Photo");
this.lightboxDimension.height = this.getScreenHeight();
this.updateActivePhoto(this.activePhotoId);
}
} }
public nextImage() { this.lightboxDimension = selectedPhoto.getDimension();
this.disableAnimation(); this.lightboxDimension.top -= this.getBodyScrollTop();
if (this.activePhotoId + 1 < this.gridPhotoQL.length) { this.blackCanvasOpacity = 0;
this.showPhoto(this.activePhotoId + 1); this.photoDimension = selectedPhoto.getDimension();
if (this.activePhotoId + 3 >= this.gridPhotoQL.length) {
this.onLastElement.emit({}); //trigger to render more photos if there are
}
return;
}
console.warn("can't find photo to show next");
}
public prevImage() { //disable scroll
this.disableAnimation(); this.overlayService.showOverlay();
setImmediate(() => {
this.lightboxDimension = <Dimension>{
top: 0,
left: 0,
width: this.getScreenWidth(),
height: this.getScreenHeight()
};
this.blackCanvasOpacity = 1.0;
this.showPhoto(this.gridPhotoQL.toArray().indexOf(selectedPhoto));
});
}
public hide() {
this.enableAnimation();
this.fullScreenService.exitFullScreen();
this.lightboxDimension = this.activePhoto.getDimension();
this.lightboxDimension.top -= this.getBodyScrollTop();
this.blackCanvasOpacity = 0;
this.photoDimension = this.activePhoto.getDimension();
setTimeout(() => {
this.visible = false;
this.activePhoto = null;
this.overlayService.hideOverlay();
}, 500);
}
setGridPhotoQL(value: QueryList<GalleryPhotoComponent>) {
if (this.changeSubscription != null) {
this.changeSubscription.unsubscribe();
}
this.gridPhotoQL = value;
this.changeSubscription = this.gridPhotoQL.changes.subscribe(() => {
if (this.activePhotoId != null && this.gridPhotoQL.length > this.activePhotoId) {
this.updateActivePhoto(this.activePhotoId);
}
});
}
private findPhotoComponent(photo: any) {
let galleryPhotoComponents = this.gridPhotoQL.toArray();
for (let i = 0; i < galleryPhotoComponents.length; i++) {
if (galleryPhotoComponents[i].gridPhoto.photo == photo) {
return galleryPhotoComponents[i];
}
}
return null;
}
//noinspection JSUnusedGlobalSymbols
@HostListener('window:keydown', ['$event'])
onKeyPress(e: KeyboardEvent) {
if (this.visible != true) {
return;
}
let event: KeyboardEvent = window.event ? <any>window.event : e;
switch (event.keyCode) {
case 37:
if (this.activePhotoId > 0) { if (this.activePhotoId > 0) {
this.showPhoto(this.activePhotoId - 1); this.prevImage();
return;
} }
console.warn("can't find photo to show prev"); break;
} case 39:
if (this.activePhotoId < this.gridPhotoQL.length - 1) {
this.nextImage();
activePhotoId: number = null;
private showPhoto(photoIndex: number) {
this.activePhoto = null;
this.changeDetector.detectChanges();
this.updateActivePhoto(photoIndex);
}
private updateActivePhoto(photoIndex: number) {
let pcList = this.gridPhotoQL.toArray();
if (photoIndex < 0 || photoIndex > this.gridPhotoQL.length) {
throw new Error("Can't find the photo");
} }
this.activePhotoId = photoIndex; break;
this.activePhoto = pcList[photoIndex]; case 27: //escape
this.hide();
this.photoDimension = this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.photo); break;
this.navigation.hasPrev = photoIndex > 0;
this.navigation.hasNext = photoIndex + 1 < pcList.length;
let to = this.activePhoto.getDimension();
//if target image out of screen -> scroll to there
if (this.getBodyScrollTop() > to.top || this.getBodyScrollTop() + this.getScreenHeight() < to.top) {
this.setBodyScrollTop(to.top);
}
} }
}
public show(photo: PhotoDTO) { private enableAnimation() {
this.enableAnimation(); this.transition = null;
this.visible = true; }
let selectedPhoto = this.findPhotoComponent(photo);
if (selectedPhoto === null) {
throw new Error("Can't find Photo");
}
this.lightboxDimension = selectedPhoto.getDimension(); private disableAnimation() {
this.lightboxDimension.top -= this.getBodyScrollTop(); this.transition = "initial";
this.blackCanvasOpacity = 0; }
this.photoDimension = selectedPhoto.getDimension();
//disable scroll
this.overlayService.showOverlay(); private getBodyScrollTop(): number {
setImmediate(() => { return window.scrollY;
this.lightboxDimension = <Dimension>{ }
top: 0,
left: 0, private setBodyScrollTop(value: number) {
width: this.getScreenWidth(), window.scrollTo(window.scrollX, value);
height: this.getScreenHeight() }
};
this.blackCanvasOpacity = 1.0; private getScreenWidth() {
this.showPhoto(this.gridPhotoQL.toArray().indexOf(selectedPhoto)); return window.innerWidth;
}); }
private getScreenHeight() {
return window.innerHeight;
}
private calcLightBoxPhotoDimension(photo: PhotoDTO): Dimension {
let width = 0;
let height = 0;
if (photo.metadata.size.height > photo.metadata.size.width) {
width = Math.round(photo.metadata.size.width * (this.getScreenHeight() / photo.metadata.size.height));
height = this.getScreenHeight();
} else {
width = this.getScreenWidth();
height = Math.round(photo.metadata.size.height * (this.getScreenWidth() / photo.metadata.size.width));
} }
let top = (this.getScreenHeight() / 2 - height / 2);
let left = (this.getScreenWidth() / 2 - width / 2);
public hide() { return <Dimension>{top: top, left: left, width: width, height: height};
this.enableAnimation(); }
this.fullScreenService.exitFullScreen();
this.lightboxDimension = this.activePhoto.getDimension();
this.lightboxDimension.top -= this.getBodyScrollTop();
this.blackCanvasOpacity = 0;
this.photoDimension = this.activePhoto.getDimension();
setTimeout(() => {
this.visible = false;
this.activePhoto = null;
this.overlayService.hideOverlay();
}, 500);
}
setGridPhotoQL(value: QueryList<GalleryPhotoComponent>) {
if (this.changeSubscription != null) {
this.changeSubscription.unsubscribe();
}
this.gridPhotoQL = value;
this.changeSubscription = this.gridPhotoQL.changes.subscribe(() => {
if (this.activePhotoId != null && this.gridPhotoQL.length > this.activePhotoId) {
this.updateActivePhoto(this.activePhotoId);
}
});
}
private findPhotoComponent(photo: any) {
let galleryPhotoComponents = this.gridPhotoQL.toArray();
for (let i = 0; i < galleryPhotoComponents.length; i++) {
if (galleryPhotoComponents[i].gridPhoto.photo == photo) {
return galleryPhotoComponents[i];
}
}
return null;
}
//noinspection JSUnusedGlobalSymbols
@HostListener('window:keydown', ['$event'])
onKeyPress(e: KeyboardEvent) {
if (this.visible != true) {
return;
}
let event: KeyboardEvent = window.event ? <any>window.event : e;
switch (event.keyCode) {
case 37:
if (this.activePhotoId > 0) {
this.prevImage();
}
break;
case 39:
if (this.activePhotoId < this.gridPhotoQL.length - 1) {
this.nextImage();
}
break;
case 27: //escape
this.hide();
break;
}
}
private enableAnimation() {
this.transition = null;
}
private disableAnimation() {
this.transition = "initial";
}
private getBodyScrollTop(): number {
return window.scrollY;
}
private setBodyScrollTop(value: number) {
window.scrollTo(window.scrollX, value);
}
private getScreenWidth() {
return window.innerWidth;
}
private getScreenHeight() {
return window.innerHeight;
}
private calcLightBoxPhotoDimension(photo: PhotoDTO): Dimension {
let width = 0;
let height = 0;
if (photo.metadata.size.height > photo.metadata.size.width) {
width = Math.round(photo.metadata.size.width * (this.getScreenHeight() / photo.metadata.size.height));
height = this.getScreenHeight();
} else {
width = this.getScreenWidth();
height = Math.round(photo.metadata.size.height * (this.getScreenWidth() / photo.metadata.size.width));
}
let top = (this.getScreenHeight() / 2 - height / 2);
let left = (this.getScreenWidth() / 2 - width / 2);
return <Dimension>{top: top, left: left, width: width, height: height};
}
} }

View File

@@ -2,64 +2,64 @@ import {Component, Input, OnChanges} from "@angular/core";
import {GridPhoto} from "../../grid/GridPhoto"; import {GridPhoto} from "../../grid/GridPhoto";
@Component({ @Component({
selector: 'gallery-lightbox-photo', selector: 'gallery-lightbox-photo',
styleUrls: ['app/gallery/lightbox/photo/photo.lightbox.gallery.component.css'], styleUrls: ['./photo.lightbox.gallery.component.css'],
templateUrl: 'app/gallery/lightbox/photo/photo.lightbox.gallery.component.html' templateUrl: './photo.lightbox.gallery.component.html'
}) })
export class GalleryLightboxPhotoComponent implements OnChanges { export class GalleryLightboxPhotoComponent implements OnChanges {
@Input() gridPhoto: GridPhoto; @Input() gridPhoto: GridPhoto;
public imageSize = {width: "auto", height: "100"}; public imageSize = {width: "auto", height: "100"};
imageLoaded: boolean = false; imageLoaded: boolean = false;
constructor() { constructor() {
}
ngOnChanges() {
this.imageLoaded = false;
this.setImageSize();
}
private setImageSize() {
if (!this.gridPhoto) {
return;
} }
ngOnChanges() { if (this.gridPhoto.photo.metadata.size.height > this.gridPhoto.photo.metadata.size.width) {
this.imageSize.height = "100";
this.imageLoaded = false; this.imageSize.width = null;
this.setImageSize(); } else {
} this.imageSize.height = null;
this.imageSize.width = "100";
private setImageSize() {
if (!this.gridPhoto) {
return;
}
if (this.gridPhoto.photo.metadata.size.height > this.gridPhoto.photo.metadata.size.width) {
this.imageSize.height = "100";
this.imageSize.width = null;
} else {
this.imageSize.height = null;
this.imageSize.width = "100";
}
} }
}
onImageLoad() { onImageLoad() {
this.imageLoaded = true; this.imageLoaded = true;
} }
onImageError() { onImageError() {
//TODO:handle error //TODO:handle error
console.error("cant load image"); console.error("cant load image");
} }
public showThumbnail(): boolean { public showThumbnail(): boolean {
return this.gridPhoto && !this.imageLoaded && return this.gridPhoto && !this.imageLoaded &&
(this.gridPhoto.isThumbnailAvailable() || this.gridPhoto.isReplacementThumbnailAvailable()); (this.gridPhoto.isThumbnailAvailable() || this.gridPhoto.isReplacementThumbnailAvailable());
} }
public thumbnailPath(): string { public thumbnailPath(): string {
if (this.gridPhoto.isThumbnailAvailable() === true) if (this.gridPhoto.isThumbnailAvailable() === true)
return this.gridPhoto.getThumbnailPath(); return this.gridPhoto.getThumbnailPath();
if (this.gridPhoto.isReplacementThumbnailAvailable() === true) if (this.gridPhoto.isReplacementThumbnailAvailable() === true)
return this.gridPhoto.getReplacementThumbnailPath(); return this.gridPhoto.getReplacementThumbnailPath();
return null return null
} }
} }

View File

@@ -7,150 +7,150 @@ import {IconThumbnail, ThumbnailManagerService} from "../../thumnailManager.serv
import {IconPhoto} from "../../IconPhoto"; import {IconPhoto} from "../../IconPhoto";
@Component({ @Component({
selector: 'gallery-map-lightbox', selector: 'gallery-map-lightbox',
styleUrls: ['app/gallery/map/lightbox/lightbox.map.gallery.component.css'], styleUrls: ['./lightbox.map.gallery.component.css'],
templateUrl: 'app/gallery/map/lightbox/lightbox.map.gallery.component.html', templateUrl: './lightbox.map.gallery.component.html',
}) })
export class GalleryMapLightboxComponent implements OnChanges { export class GalleryMapLightboxComponent implements OnChanges {
@Input() photos: Array<PhotoDTO>; @Input() photos: Array<PhotoDTO>;
private startPosition = null; private startPosition = null;
public lightboxDimension: Dimension = <Dimension>{top: 0, left: 0, width: 0, height: 0}; public lightboxDimension: Dimension = <Dimension>{top: 0, left: 0, width: 0, height: 0};
public mapDimension: Dimension = <Dimension>{top: 0, left: 0, width: 0, height: 0}; public mapDimension: Dimension = <Dimension>{top: 0, left: 0, width: 0, height: 0};
private visible = false; public visible = false;
private opacity = 1.0; public opacity = 1.0;
mapPhotos: Array<{latitude: number, longitude: number, iconUrl?: string, thumbnail: IconThumbnail}> = []; mapPhotos: Array<{ latitude: number, longitude: number, iconUrl?: string, thumbnail: IconThumbnail }> = [];
mapCenter = {latitude: 0, longitude: 0}; mapCenter = {latitude: 0, longitude: 0};
@ViewChild("root") elementRef: ElementRef; @ViewChild("root") elementRef: ElementRef;
@ViewChild(AgmMap) map: AgmMap; @ViewChild(AgmMap) map: AgmMap;
constructor(private fullScreenService: FullScreenService, private thumbnailService: ThumbnailManagerService) { constructor(public fullScreenService: FullScreenService, private thumbnailService: ThumbnailManagerService) {
} }
//TODO: fix zooming //TODO: fix zooming
ngOnChanges() { ngOnChanges() {
if (this.visible == false) { if (this.visible == false) {
return; return;
} }
this.showImages(); this.showImages();
}
public show(position: Dimension) {
this.visible = true;
this.opacity = 1.0;
this.startPosition = position;
this.lightboxDimension = position;
this.lightboxDimension.top -= this.getBodyScrollTop();
this.mapDimension = <Dimension>{
top: 0,
left: 0,
width: this.getScreenWidth(),
height: this.getScreenHeight()
};
this.map.triggerResize();
document.getElementsByTagName('body')[0].style.overflow = 'hidden';
this.showImages();
setImmediate(() => {
this.lightboxDimension = <Dimension>{
top: 0,
left: 0,
width: this.getScreenWidth(),
height: this.getScreenHeight()
};
});
}
public hide() {
this.fullScreenService.exitFullScreen();
let to = this.startPosition;
//iff target image out of screen -> scroll to there
if (this.getBodyScrollTop() > to.top || this.getBodyScrollTop() + this.getScreenHeight() < to.top) {
this.setBodyScrollTop(to.top);
} }
public show(position: Dimension) { this.lightboxDimension = this.startPosition;
this.visible = true; this.lightboxDimension.top -= this.getBodyScrollTop();
this.opacity = 1.0; document.getElementsByTagName('body')[0].style.overflow = 'scroll';
this.startPosition = position; this.opacity = 0.0;
this.lightboxDimension = position; setTimeout(() => {
this.lightboxDimension.top -= this.getBodyScrollTop(); this.visible = false;
this.mapDimension = <Dimension>{ this.hideImages();
top: 0, }, 500);
left: 0,
width: this.getScreenWidth(),
height: this.getScreenHeight() }
showImages() {
this.hideImages();
this.mapPhotos = this.photos.filter(p => {
return p.metadata && p.metadata.positionData && p.metadata.positionData.GPSData;
}).map(p => {
let th = this.thumbnailService.getIcon(new IconPhoto(p));
let obj: { latitude: number, longitude: number, iconUrl?: string, thumbnail: IconThumbnail } = {
latitude: p.metadata.positionData.GPSData.latitude,
longitude: p.metadata.positionData.GPSData.longitude,
thumbnail: th
};
if (th.Available == true) {
obj.iconUrl = th.Src;
} else {
th.OnLoad = () => {
obj.iconUrl = th.Src;
}; };
this.map.triggerResize(); }
return obj;
});
document.getElementsByTagName('body')[0].style.overflow = 'hidden'; if (this.mapPhotos.length > 0) {
this.showImages(); this.mapCenter = this.mapPhotos[0];
setImmediate(() => {
this.lightboxDimension = <Dimension>{
top: 0,
left: 0,
width: this.getScreenWidth(),
height: this.getScreenHeight()
};
});
} }
}
public hide() { hideImages() {
this.fullScreenService.exitFullScreen(); this.mapPhotos.forEach(mp => mp.thumbnail.destroy());
let to = this.startPosition; this.mapPhotos = [];
}
//iff target image out of screen -> scroll to there
if (this.getBodyScrollTop() > to.top || this.getBodyScrollTop() + this.getScreenHeight() < to.top) {
this.setBodyScrollTop(to.top);
}
this.lightboxDimension = this.startPosition;
this.lightboxDimension.top -= this.getBodyScrollTop();
document.getElementsByTagName('body')[0].style.overflow = 'scroll';
this.opacity = 0.0;
setTimeout(() => {
this.visible = false;
this.hideImages();
}, 500);
private getBodyScrollTop(): number {
return window.scrollY;
}
private setBodyScrollTop(value: number) {
window.scrollTo(window.scrollX, value);
}
private getScreenWidth() {
return window.innerWidth;
}
private getScreenHeight() {
return window.innerHeight;
}
//noinspection JSUnusedGlobalSymbols
@HostListener('window:keydown', ['$event'])
onKeyPress(e: KeyboardEvent) {
if (this.visible != true) {
return;
} }
let event: KeyboardEvent = window.event ? <any>window.event : e;
showImages() { switch (event.keyCode) {
this.hideImages(); case 27: //escape
this.hide();
this.mapPhotos = this.photos.filter(p => { break;
return p.metadata && p.metadata.positionData && p.metadata.positionData.GPSData;
}).map(p => {
let th = this.thumbnailService.getIcon(new IconPhoto(p));
let obj: {latitude: number, longitude: number, iconUrl?: string, thumbnail: IconThumbnail} = {
latitude: p.metadata.positionData.GPSData.latitude,
longitude: p.metadata.positionData.GPSData.longitude,
thumbnail: th
};
if (th.Available == true) {
obj.iconUrl = th.Src;
} else {
th.OnLoad = () => {
obj.iconUrl = th.Src;
};
}
return obj;
});
if (this.mapPhotos.length > 0) {
this.mapCenter = this.mapPhotos[0];
}
}
hideImages() {
this.mapPhotos.forEach(mp => mp.thumbnail.destroy());
this.mapPhotos = [];
}
private getBodyScrollTop(): number {
return window.scrollY;
}
private setBodyScrollTop(value: number) {
window.scrollTo(window.scrollX, value);
}
private getScreenWidth() {
return window.innerWidth;
}
private getScreenHeight() {
return window.innerHeight;
}
//noinspection JSUnusedGlobalSymbols
@HostListener('window:keydown', ['$event'])
onKeyPress(e: KeyboardEvent) {
if (this.visible != true) {
return;
}
let event: KeyboardEvent = window.event ? <any>window.event : e;
switch (event.keyCode) {
case 27: //escape
this.hide();
break;
}
} }
}
} }

View File

@@ -1,50 +1,50 @@
import {Component, OnChanges, Input, ViewChild, ElementRef} from "@angular/core"; import {Component, ElementRef, Input, OnChanges, ViewChild} from "@angular/core";
import {PhotoDTO} from "../../../../common/entities/PhotoDTO"; import {PhotoDTO} from "../../../../common/entities/PhotoDTO";
import {IRenderable, Dimension} from "../../model/IRenderable"; import {Dimension, IRenderable} from "../../model/IRenderable";
import {GalleryMapLightboxComponent} from "./lightbox/lightbox.map.gallery.component"; import {GalleryMapLightboxComponent} from "./lightbox/lightbox.map.gallery.component";
@Component({ @Component({
selector: 'gallery-map', selector: 'gallery-map',
templateUrl: 'app/gallery/map/map.gallery.component.html', templateUrl: './map.gallery.component.html',
styleUrls: ['app/gallery/map/map.gallery.component.css'] styleUrls: ['./map.gallery.component.css']
}) })
export class GalleryMapComponent implements OnChanges, IRenderable { export class GalleryMapComponent implements OnChanges, IRenderable {
@Input() photos: Array<PhotoDTO>; @Input() photos: Array<PhotoDTO>;
@ViewChild(GalleryMapLightboxComponent) mapLightbox: GalleryMapLightboxComponent; @ViewChild(GalleryMapLightboxComponent) mapLightbox: GalleryMapLightboxComponent;
mapPhotos: Array<{latitude: number, longitude: number}> = []; mapPhotos: Array<{ latitude: number, longitude: number }> = [];
mapCenter = {latitude: 0, longitude: 0}; mapCenter = {latitude: 0, longitude: 0};
@ViewChild("map") map: ElementRef; @ViewChild("map") map: ElementRef;
//TODO: fix zooming
ngOnChanges() {
this.mapPhotos = this.photos.filter(p => {
return p.metadata && p.metadata.positionData && p.metadata.positionData.GPSData;
}).map(p => {
return {
latitude: p.metadata.positionData.GPSData.latitude,
longitude: p.metadata.positionData.GPSData.longitude
};
});
if (this.mapPhotos.length > 0) {
this.mapCenter = this.mapPhotos[0];
}
//TODO: fix zooming
ngOnChanges() {
this.mapPhotos = this.photos.filter(p => {
return p.metadata && p.metadata.positionData && p.metadata.positionData.GPSData;
}).map(p => {
return {
latitude: p.metadata.positionData.GPSData.latitude,
longitude: p.metadata.positionData.GPSData.longitude
};
});
if (this.mapPhotos.length > 0) {
this.mapCenter = this.mapPhotos[0];
} }
click() {
this.mapLightbox.show(this.getDimension());
}
public getDimension(): Dimension { }
return <Dimension>{
top: this.map.nativeElement.offsetTop, click() {
left: this.map.nativeElement.offsetLeft, this.mapLightbox.show(this.getDimension());
width: this.map.nativeElement.offsetWidth, }
height: this.map.nativeElement.offsetHeight
}; public getDimension(): Dimension {
} return <Dimension>{
top: this.map.nativeElement.offsetTop,
left: this.map.nativeElement.offsetLeft,
width: this.map.nativeElement.offsetWidth,
height: this.map.nativeElement.offsetHeight
};
}
} }

View File

@@ -3,68 +3,68 @@ import {DirectoryDTO} from "../../../../common/entities/DirectoryDTO";
import {RouterLink} from "@angular/router"; import {RouterLink} from "@angular/router";
@Component({ @Component({
selector: 'gallery-navbar', selector: 'gallery-navbar',
templateUrl: 'app/gallery/navigator/navigator.gallery.component.html', templateUrl: './navigator.gallery.component.html',
providers: [RouterLink], providers: [RouterLink],
}) })
export class GalleryNavigatorComponent implements OnChanges { export class GalleryNavigatorComponent implements OnChanges {
@Input() directory: DirectoryDTO; @Input() directory: DirectoryDTO;
routes: Array<any> = []; routes: Array<any> = [];
constructor() { constructor() {
}
ngOnChanges() {
this.getPath();
}
getPath(): any {
if (!this.directory) {
return [];
}
let path = this.directory.path.replace(new RegExp("\\\\", 'g'), "/");
let dirs = path.split("/");
dirs.push(this.directory.name);
//removing empty strings
for (let i = 0; i < dirs.length; i++) {
if (!dirs[i] || 0 === dirs[i].length || "." === dirs[i]) {
dirs.splice(i, 1);
i--;
}
} }
ngOnChanges() { let arr: any = [];
this.getPath();
}
getPath(): any {
if (!this.directory) {
return [];
}
let path = this.directory.path.replace(new RegExp("\\\\", 'g'), "/");
let dirs = path.split("/");
dirs.push(this.directory.name);
//removing empty strings
for (let i = 0; i < dirs.length; i++) {
if (!dirs[i] || 0 === dirs[i].length || "." === dirs[i]) {
dirs.splice(i, 1);
i--;
}
}
let arr: any = [];
//create root link
if (dirs.length == 0) {
arr.push({name: "Images", route: null});
} else {
arr.push({name: "Images", route: "/"});
}
//create rest navigation
dirs.forEach((name, index) => {
let route = dirs.slice(0, dirs.indexOf(name) + 1).join("/");
if (dirs.length - 1 == index) {
arr.push({name: name, route: null});
} else {
arr.push({name: name, route: route});
}
});
this.routes = arr;
//create root link
if (dirs.length == 0) {
arr.push({name: "Images", route: null});
} else {
arr.push({name: "Images", route: "/"});
} }
//create rest navigation
dirs.forEach((name, index) => {
let route = dirs.slice(0, dirs.indexOf(name) + 1).join("/");
if (dirs.length - 1 == index) {
arr.push({name: name, route: null});
} else {
arr.push({name: name, route: route});
}
});
this.routes = arr;
}
} }

View File

@@ -4,57 +4,57 @@ import {Event} from "../../../common/event/Event";
@Injectable() @Injectable()
export class OverlayService { export class OverlayService {
OnOverlayChange = new Event<boolean>(); OnOverlayChange = new Event<boolean>();
private scrollWidth: number = null; private scrollWidth: number = null;
public showOverlay() { public showOverlay() {
//disable scrolling //disable scrolling
document.getElementsByTagName('body')[0].style.overflow = 'hidden'; document.getElementsByTagName('body')[0].style.overflow = 'hidden';
this.OnOverlayChange.trigger(true); this.OnOverlayChange.trigger(true);
}
public hideOverlay() {
document.getElementsByTagName('body')[0].style.overflowY = 'scroll';
this.OnOverlayChange.trigger(false);
}
getScrollbarWidth() {
if (this.scrollWidth == null) {
let outer = document.createElement("div");
outer.style.visibility = "hidden";
outer.style.width = "100px";
outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps
document.body.appendChild(outer);
let widthNoScroll = outer.offsetWidth;
// force scrollbars
outer.style.overflow = "scroll";
// add innerdiv
let inner = document.createElement("div");
inner.style.width = "100%";
outer.appendChild(inner);
let widthWithScroll = inner.offsetWidth;
// remove divs
outer.parentNode.removeChild(outer);
this.scrollWidth = widthNoScroll - widthWithScroll;
} }
public hideOverlay() { return this.scrollWidth;
}
document.getElementsByTagName('body')[0].style.overflowY = 'scroll'; getPhantomScrollbarWidth() {
this.OnOverlayChange.trigger(false); if (document.getElementsByTagName('body')[0].style.overflow == 'hidden') {
} return this.getScrollbarWidth();
getScrollbarWidth() {
if (this.scrollWidth == null) {
let outer = document.createElement("div");
outer.style.visibility = "hidden";
outer.style.width = "100px";
outer.style.msOverflowStyle = "scrollbar"; // needed for WinJS apps
document.body.appendChild(outer);
let widthNoScroll = outer.offsetWidth;
// force scrollbars
outer.style.overflow = "scroll";
// add innerdiv
let inner = document.createElement("div");
inner.style.width = "100%";
outer.appendChild(inner);
let widthWithScroll = inner.offsetWidth;
// remove divs
outer.parentNode.removeChild(outer);
this.scrollWidth = widthNoScroll - widthWithScroll;
}
return this.scrollWidth;
}
getPhantomScrollbarWidth() {
if (document.getElementsByTagName('body')[0].style.overflow == 'hidden') {
return this.getScrollbarWidth();
}
return 0;
} }
return 0;
}
} }

View File

@@ -7,12 +7,12 @@ import {Message} from "../../../../common/entities/Message";
export class AutoCompleteService { export class AutoCompleteService {
constructor(private _networkService:NetworkService) { constructor(private _networkService: NetworkService) {
} }
public autoComplete(text:string):Promise<Message<Array<AutoCompleteItem> >> { public autoComplete(text: string): Promise<Message<Array<AutoCompleteItem>>> {
return this._networkService.getJson("/autocomplete/" + text); return this._networkService.getJson("/autocomplete/" + text);
} }
} }

View File

@@ -2,7 +2,7 @@
<form class="navbar-form" role="search" #SearchForm="ngForm"> <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)="onSearchChange($event)" <input type="text" class="form-control" placeholder="Search" (keyup)="onSearchChange($event)"
(blur)="onFocusLost($event)" (focus)="onFocus($evnet)" [(ngModel)]="searchText" #name="ngModel" (blur)="onFocusLost()" (focus)="onFocus()" [(ngModel)]="searchText" #name="ngModel"
ngControl="search" ngControl="search"
name="srch-term" id="srch-term" autocomplete="off"> name="srch-term" id="srch-term" autocomplete="off">

View File

@@ -7,134 +7,134 @@ import {GalleryService} from "../gallery.service";
import {Config} from "../../../../common/config/public/Config"; import {Config} from "../../../../common/config/public/Config";
@Component({ @Component({
selector: 'gallery-search', selector: 'gallery-search',
templateUrl: 'app/gallery/search/search.gallery.component.html', templateUrl: './search.gallery.component.html',
styleUrls: ['app/gallery/search/search.gallery.component.css'], styleUrls: ['./search.gallery.component.css'],
providers: [AutoCompleteService, RouterLink] providers: [AutoCompleteService, RouterLink]
}) })
export class GallerySearchComponent { export class GallerySearchComponent {
autoCompleteItems: Array<AutoCompleteRenderItem> = []; autoCompleteItems: Array<AutoCompleteRenderItem> = [];
private searchText: string = ""; public searchText: string = "";
private cache = { private cache = {
lastAutocomplete: "", lastAutocomplete: "",
lastInstantSearch: "" lastInstantSearch: ""
}; };
SearchTypes: any = []; SearchTypes: any = [];
constructor(private _autoCompleteService: AutoCompleteService, constructor(private _autoCompleteService: AutoCompleteService,
private _galleryService: GalleryService, private _galleryService: GalleryService,
private _route: ActivatedRoute) { private _route: ActivatedRoute) {
this.SearchTypes = SearchTypes; this.SearchTypes = SearchTypes;
this._route.params this._route.params
.subscribe((params: Params) => { .subscribe((params: Params) => {
let searchText = params['searchText']; let searchText = params['searchText'];
if (searchText && searchText != "") { if (searchText && searchText != "") {
this.searchText = searchText; this.searchText = searchText;
}
});
}
onSearchChange(event: KeyboardEvent) {
let searchText = (<HTMLInputElement>event.target).value.trim();
if (Config.Client.Search.autocompleteEnabled && this.cache.lastAutocomplete != searchText) {
this.cache.lastAutocomplete = searchText;
this.autocomplete(searchText);
} }
if (Config.Client.Search.instantSearchEnabled && this.cache.lastInstantSearch != searchText) { });
this.cache.lastInstantSearch = searchText; }
this._galleryService.instantSearch(searchText);
onSearchChange(event: KeyboardEvent) {
let searchText = (<HTMLInputElement>event.target).value.trim();
if (Config.Client.Search.autocompleteEnabled && this.cache.lastAutocomplete != searchText) {
this.cache.lastAutocomplete = searchText;
this.autocomplete(searchText);
}
if (Config.Client.Search.instantSearchEnabled && this.cache.lastInstantSearch != searchText) {
this.cache.lastInstantSearch = searchText;
this._galleryService.instantSearch(searchText);
}
}
public onSearch() {
if (Config.Client.Search.searchEnabled) {
this._galleryService.search(this.searchText);
}
}
public search(item: AutoCompleteItem) {
console.log("clicked");
this.searchText = item.text;
this.onSearch();
}
mouseOverAutoComplete: boolean = false;
public setMouseOverAutoComplete(value: boolean) {
this.mouseOverAutoComplete = value;
}
public onFocusLost() {
if (this.mouseOverAutoComplete == false) {
this.autoCompleteItems = [];
}
}
public onFocus() {
this.autocomplete(this.searchText);
}
private emptyAutoComplete() {
this.autoCompleteItems = [];
}
private autocomplete(searchText: string) {
if (!Config.Client.Search.autocompleteEnabled) {
return
}
if (searchText.trim().length > 0) {
this._autoCompleteService.autoComplete(searchText).then((message: Message<Array<AutoCompleteItem>>) => {
if (message.error) {
//TODO: implement
console.error(message.error);
return;
} }
this.showSuggestions(message.result, searchText);
});
} else {
this.emptyAutoComplete();
} }
}
public onSearch() { private showSuggestions(suggestions: Array<AutoCompleteItem>, searchText: string) {
if (Config.Client.Search.searchEnabled) { this.emptyAutoComplete();
this._galleryService.search(this.searchText); suggestions.forEach((item: AutoCompleteItem) => {
} let renderItem = new AutoCompleteRenderItem(item.text, searchText, item.type);
} this.autoCompleteItems.push(renderItem);
});
}
public search(item: AutoCompleteItem) { public setSearchText(searchText: string) {
console.log("clicked"); this.searchText = searchText;
this.searchText = item.text; }
this.onSearch();
}
mouseOverAutoComplete: boolean = false;
public setMouseOverAutoComplete(value: boolean) {
this.mouseOverAutoComplete = value;
}
public onFocusLost() {
if (this.mouseOverAutoComplete == false) {
this.autoCompleteItems = [];
}
}
public onFocus() {
this.autocomplete(this.searchText);
}
private emptyAutoComplete() {
this.autoCompleteItems = [];
}
private autocomplete(searchText: string) {
if (!Config.Client.Search.autocompleteEnabled) {
return
}
if (searchText.trim().length > 0) {
this._autoCompleteService.autoComplete(searchText).then((message: Message<Array<AutoCompleteItem>>) => {
if (message.error) {
//TODO: implement
console.error(message.error);
return;
}
this.showSuggestions(message.result, searchText);
});
} else {
this.emptyAutoComplete();
}
}
private showSuggestions(suggestions: Array<AutoCompleteItem>, searchText: string) {
this.emptyAutoComplete();
suggestions.forEach((item: AutoCompleteItem) => {
let renderItem = new AutoCompleteRenderItem(item.text, searchText, item.type);
this.autoCompleteItems.push(renderItem);
});
}
public setSearchText(searchText: string) {
this.searchText = searchText;
}
} }
class AutoCompleteRenderItem { class AutoCompleteRenderItem {
public preText: string = ""; public preText: string = "";
public highLightText: string = ""; public highLightText: string = "";
public postText: string = ""; public postText: string = "";
public type: SearchTypes; public type: SearchTypes;
constructor(public text: string, searchText: string, type: SearchTypes) { constructor(public text: string, searchText: string, type: SearchTypes) {
let preIndex = text.toLowerCase().indexOf(searchText.toLowerCase()); let preIndex = text.toLowerCase().indexOf(searchText.toLowerCase());
if (preIndex > -1) { if (preIndex > -1) {
this.preText = text.substring(0, preIndex); this.preText = text.substring(0, preIndex);
this.highLightText = text.substring(preIndex, preIndex + searchText.length); this.highLightText = text.substring(preIndex, preIndex + searchText.length);
this.postText = text.substring(preIndex + searchText.length); this.postText = text.substring(preIndex + searchText.length);
} else { } else {
this.postText = text; this.postText = text;
}
this.type = type;
} }
this.type = type;
}
} }

View File

@@ -6,208 +6,208 @@ import {PhotoDTO} from "../../../common/entities/PhotoDTO";
import {Config} from "../../../common/config/public/Config"; import {Config} from "../../../common/config/public/Config";
export enum ThumbnailLoadingPriority{ export enum ThumbnailLoadingPriority{
high, medium, low high, medium, low
} }
@Injectable() @Injectable()
export class ThumbnailLoaderService { export class ThumbnailLoaderService {
que: Array<ThumbnailTask> = []; que: Array<ThumbnailTask> = [];
runningRequests: number = 0; runningRequests: number = 0;
constructor(private galleryChacheService: GalleryCacheService) { constructor(private galleryChacheService: GalleryCacheService) {
}
removeTasks() {
this.que = [];
}
removeTask(taskEntry: ThumbnailTaskEntity) {
for (let i = 0; i < this.que.length; i++) {
let index = this.que[i].taskEntities.indexOf(taskEntry);
if (index == -1) {
this.que[i].taskEntities.splice(index, 1);
if (this.que[i].taskEntities.length == 0) {
this.que.splice(i, 1);
}
return;
}
} }
removeTasks() { }
this.que = [];
loadIcon(photo: IconPhoto, priority: ThumbnailLoadingPriority, listener: ThumbnailLoadingListener): ThumbnailTaskEntity {
let tmp: ThumbnailTask = null;
//is image already qued?
for (let i = 0; i < this.que.length; i++) {
if (this.que[i].path == photo.getIconPath()) {
tmp = this.que[i];
break;
}
} }
removeTask(taskEntry: ThumbnailTaskEntity) { let thumbnailTaskEntity = {priority: priority, listener: listener};
//add to previous
if (tmp != null) {
tmp.taskEntities.push(thumbnailTaskEntity);
if (tmp.inProgress == true) {
listener.onStartedLoading();
}
for (let i = 0; i < this.que.length; i++) {
let index = this.que[i].taskEntities.indexOf(taskEntry);
if (index == -1) {
this.que[i].taskEntities.splice(index, 1);
if (this.que[i].taskEntities.length == 0) {
this.que.splice(i, 1);
} } else {//create new task
return; this.que.push(<ThumbnailTask>{
} photo: photo.photo,
} inProgress: false,
taskEntities: [thumbnailTaskEntity],
onLoaded: () => {
photo.iconLoaded();
},
path: photo.getIconPath()
});
}
setImmediate(this.run);
return thumbnailTaskEntity;
}
loadImage(photo: Photo, priority: ThumbnailLoadingPriority, listener: ThumbnailLoadingListener): ThumbnailTaskEntity {
let tmp: ThumbnailTask = null;
//is image already qued?
for (let i = 0; i < this.que.length; i++) {
if (this.que[i].path == photo.getThumbnailPath()) {
tmp = this.que[i];
break;
}
} }
loadIcon(photo: IconPhoto, priority: ThumbnailLoadingPriority, listener: ThumbnailLoadingListener): ThumbnailTaskEntity { let thumbnailTaskEntity = {priority: priority, listener: listener};
let tmp: ThumbnailTask = null; //add to previous
//is image already qued? if (tmp != null) {
for (let i = 0; i < this.que.length; i++) { tmp.taskEntities.push(thumbnailTaskEntity);
if (this.que[i].path == photo.getIconPath()) { if (tmp.inProgress == true) {
tmp = this.que[i]; listener.onStartedLoading();
break; }
}
}
let thumbnailTaskEntity = {priority: priority, listener: listener};
//add to previous
if (tmp != null) {
tmp.taskEntities.push(thumbnailTaskEntity);
if (tmp.inProgress == true) {
listener.onStartedLoading();
}
} else {//create new task } else {//create new task
this.que.push(<ThumbnailTask>{ this.que.push({
photo: photo.photo, photo: photo.photo,
inProgress: false, inProgress: false,
taskEntities: [thumbnailTaskEntity], taskEntities: [thumbnailTaskEntity],
onLoaded: () => { onLoaded: () => {
photo.iconLoaded(); photo.thumbnailLoaded();
}, },
path: photo.getIconPath() path: photo.getThumbnailPath()
}); });
} }
setImmediate(this.run); setImmediate(this.run);
return thumbnailTaskEntity; return thumbnailTaskEntity;
}
private getNextTask(): ThumbnailTask {
if (this.que.length === 0) {
return null;
} }
loadImage(photo: Photo, priority: ThumbnailLoadingPriority, listener: ThumbnailLoadingListener): ThumbnailTaskEntity { for (let i = 0; i < this.que.length; i++) {
for (let j = 0; j < this.que[i].taskEntities.length; j++) {
let tmp: ThumbnailTask = null; if (this.que[i].inProgress == false && this.que[i].taskEntities[j].priority === ThumbnailLoadingPriority.high) {
//is image already qued? return this.que[i];
for (let i = 0; i < this.que.length; i++) {
if (this.que[i].path == photo.getThumbnailPath()) {
tmp = this.que[i];
break;
}
} }
}
}
let thumbnailTaskEntity = {priority: priority, listener: listener}; for (let i = 0; i < this.que.length; i++) {
//add to previous for (let j = 0; j < this.que[i].taskEntities.length; j++) {
if (tmp != null) { if (this.que[i].inProgress == false && this.que[i].taskEntities[j].priority === ThumbnailLoadingPriority.medium) {
tmp.taskEntities.push(thumbnailTaskEntity); return this.que[i];
if (tmp.inProgress == true) {
listener.onStartedLoading();
}
} else {//create new task
this.que.push({
photo: photo.photo,
inProgress: false,
taskEntities: [thumbnailTaskEntity],
onLoaded: () => {
photo.thumbnailLoaded();
},
path: photo.getThumbnailPath()
});
} }
setImmediate(this.run); }
return thumbnailTaskEntity;
} }
private getNextTask(): ThumbnailTask { for (let i = 0; i < this.que.length; i++) {
if (this.que.length === 0) { if (this.que[i].inProgress == false) {
return null; return this.que[i];
} }
for (let i = 0; i < this.que.length; i++) {
for (let j = 0; j < this.que[i].taskEntities.length; j++) {
if (this.que[i].inProgress == false && this.que[i].taskEntities[j].priority === ThumbnailLoadingPriority.high) {
return this.que[i];
}
}
}
for (let i = 0; i < this.que.length; i++) {
for (let j = 0; j < this.que[i].taskEntities.length; j++) {
if (this.que[i].inProgress == false && this.que[i].taskEntities[j].priority === ThumbnailLoadingPriority.medium) {
return this.que[i];
}
}
}
for (let i = 0; i < this.que.length; i++) {
if (this.que[i].inProgress == false) {
return this.que[i];
}
}
return null;
} }
private taskReady(task: ThumbnailTask) { return null;
let i = this.que.indexOf(task); }
if (i == -1) {
if (task.taskEntities.length !== 0) { private taskReady(task: ThumbnailTask) {
console.error("ThumbnailLoader: can't find task to remove"); let i = this.que.indexOf(task);
} if (i == -1) {
return; if (task.taskEntities.length !== 0) {
} console.error("ThumbnailLoader: can't find task to remove");
this.que.splice(i, 1); }
return;
}
this.que.splice(i, 1);
}
run = () => {
if (this.que.length === 0 || this.runningRequests >= Config.Client.concurrentThumbnailGenerations) {
return;
}
let task = this.getNextTask();
if (task === null) {
return;
} }
this.runningRequests++;
task.taskEntities.forEach(te => te.listener.onStartedLoading());
task.inProgress = true;
run = () => { let curImg = new Image();
if (this.que.length === 0 || this.runningRequests >= Config.Client.concurrentThumbnailGenerations) { curImg.onload = () => {
return; task.onLoaded();
} this.galleryChacheService.photoUpdated(task.photo);
let task = this.getNextTask(); task.taskEntities.forEach((te: ThumbnailTaskEntity) => te.listener.onLoad());
if (task === null) { this.taskReady(task);
return; this.runningRequests--;
} this.run();
this.runningRequests++;
task.taskEntities.forEach(te => te.listener.onStartedLoading());
task.inProgress = true;
let curImg = new Image();
curImg.onload = () => {
task.onLoaded();
this.galleryChacheService.photoUpdated(task.photo);
task.taskEntities.forEach((te: ThumbnailTaskEntity) => te.listener.onLoad());
this.taskReady(task);
this.runningRequests--;
this.run();
};
curImg.onerror = (error) => {
task.taskEntities.forEach((te: ThumbnailTaskEntity) => te.listener.onError(error));
this.taskReady(task);
this.runningRequests--;
this.run();
};
curImg.src = task.path;
}; };
curImg.onerror = (error) => {
task.taskEntities.forEach((te: ThumbnailTaskEntity) => te.listener.onError(error));
this.taskReady(task);
this.runningRequests--;
this.run();
};
curImg.src = task.path;
};
} }
export interface ThumbnailLoadingListener { export interface ThumbnailLoadingListener {
onStartedLoading: () => void; onStartedLoading: () => void;
onLoad: () => void; onLoad: () => void;
onError: (error: any) => void; onError: (error: any) => void;
} }
export interface ThumbnailTaskEntity { export interface ThumbnailTaskEntity {
priority: ThumbnailLoadingPriority; priority: ThumbnailLoadingPriority;
listener: ThumbnailLoadingListener; listener: ThumbnailLoadingListener;
} }
interface ThumbnailTask { interface ThumbnailTask {
photo: PhotoDTO; photo: PhotoDTO;
inProgress: boolean; inProgress: boolean;
taskEntities: Array<ThumbnailTaskEntity>; taskEntities: Array<ThumbnailTaskEntity>;
path: string; path: string;
onLoaded: Function; onLoaded: Function;
} }

View File

@@ -4,186 +4,186 @@ import {Photo} from "./Photo";
import {IconPhoto} from "./IconPhoto"; import {IconPhoto} from "./IconPhoto";
export enum ThumbnailLoadingPriority{ export enum ThumbnailLoadingPriority{
high, medium, low high, medium, low
} }
@Injectable() @Injectable()
export class ThumbnailManagerService { export class ThumbnailManagerService {
constructor(private thumbnailLoader: ThumbnailLoaderService) { constructor(private thumbnailLoader: ThumbnailLoaderService) {
} }
public getThumbnail(photo: Photo) { public getThumbnail(photo: Photo) {
return new Thumbnail(photo, this.thumbnailLoader); return new Thumbnail(photo, this.thumbnailLoader);
} }
public getIcon(photo: IconPhoto) { public getIcon(photo: IconPhoto) {
return new IconThumbnail(photo, this.thumbnailLoader); return new IconThumbnail(photo, this.thumbnailLoader);
} }
} }
export abstract class ThumbnailBase { export abstract class ThumbnailBase {
protected available: boolean = false; protected available: boolean = false;
protected src: string = null; protected src: string = null;
protected loading: boolean = false; protected loading: boolean = false;
protected onLoad: Function = null; protected onLoad: Function = null;
protected thumbnailTask: ThumbnailTaskEntity; protected thumbnailTask: ThumbnailTaskEntity;
constructor(protected thumbnailService: ThumbnailLoaderService) { constructor(protected thumbnailService: ThumbnailLoaderService) {
} }
abstract set Visible(visible: boolean); abstract set Visible(visible: boolean);
set OnLoad(onLoad: Function) { set OnLoad(onLoad: Function) {
this.onLoad = onLoad; this.onLoad = onLoad;
} }
get Available() { get Available() {
return this.available; return this.available;
} }
get Src() { get Src() {
return this.src; return this.src;
} }
get Loading() { get Loading() {
return this.loading; return this.loading;
} }
destroy() { destroy() {
if (this.thumbnailTask != null) { if (this.thumbnailTask != null) {
this.thumbnailService.removeTask(this.thumbnailTask); this.thumbnailService.removeTask(this.thumbnailTask);
this.thumbnailTask = null; this.thumbnailTask = null;
}
} }
}
} }
export class IconThumbnail extends ThumbnailBase { export class IconThumbnail extends ThumbnailBase {
constructor(private photo: IconPhoto, thumbnailService: ThumbnailLoaderService) { constructor(private photo: IconPhoto, thumbnailService: ThumbnailLoaderService) {
super(thumbnailService); super(thumbnailService);
this.src = ""; this.src = "";
if (this.photo.isIconAvailable()) { if (this.photo.isIconAvailable()) {
this.src = this.photo.getIconPath();
this.available = true;
if (this.onLoad) this.onLoad();
}
if (!this.photo.isIconAvailable()) {
setImmediate(() => {
let listener: ThumbnailLoadingListener = {
onStartedLoading: () => { //onLoadStarted
this.loading = true;
},
onLoad: () => {//onLoaded
this.src = this.photo.getIconPath(); this.src = this.photo.getIconPath();
this.available = true;
if (this.onLoad) this.onLoad(); if (this.onLoad) this.onLoad();
} this.available = true;
this.loading = false;
if (!this.photo.isIconAvailable()) { this.thumbnailTask = null;
setImmediate(() => { },
onError: (error) => {//onError
let listener: ThumbnailLoadingListener = { this.thumbnailTask = null;
onStartedLoading: () => { //onLoadStarted //TODO: handle error
this.loading = true; //TODO: not an error if its from cache
}, console.error("something bad happened");
onLoad: () => {//onLoaded console.error(error);
this.src = this.photo.getIconPath(); }
if (this.onLoad) this.onLoad(); };
this.available = true; this.thumbnailTask = this.thumbnailService.loadIcon(this.photo, ThumbnailLoadingPriority.high, listener);
this.loading = false;
this.thumbnailTask = null;
},
onError: (error) => {//onError
this.thumbnailTask = null;
//TODO: handle error
//TODO: not an error if its from cache
console.error("something bad happened");
console.error(error);
}
};
this.thumbnailTask = this.thumbnailService.loadIcon(this.photo, ThumbnailLoadingPriority.high, listener);
}); });
}
} }
set Visible(visible: boolean) { }
if (!this.thumbnailTask) return;
if (visible === true) {
this.thumbnailTask.priority = ThumbnailLoadingPriority.high;
} else {
this.thumbnailTask.priority = ThumbnailLoadingPriority.medium;
}
set Visible(visible: boolean) {
if (!this.thumbnailTask) return;
if (visible === true) {
this.thumbnailTask.priority = ThumbnailLoadingPriority.high;
} else {
this.thumbnailTask.priority = ThumbnailLoadingPriority.medium;
} }
}
} }
export class Thumbnail extends ThumbnailBase { export class Thumbnail extends ThumbnailBase {
constructor(private photo: Photo, thumbnailService: ThumbnailLoaderService) { constructor(private photo: Photo, thumbnailService: ThumbnailLoaderService) {
super(thumbnailService); super(thumbnailService);
if (this.photo.isThumbnailAvailable()) { if (this.photo.isThumbnailAvailable()) {
this.src = this.photo.getThumbnailPath();
this.available = true;
if (this.onLoad) this.onLoad();
} else if (this.photo.isReplacementThumbnailAvailable()) {
this.src = this.photo.getReplacementThumbnailPath();
this.available = true;
}
if (!this.photo.isThumbnailAvailable()) {
setImmediate(() => {
let listener: ThumbnailLoadingListener = {
onStartedLoading: () => { //onLoadStarted
this.loading = true;
},
onLoad: () => {//onLoaded
this.src = this.photo.getThumbnailPath(); this.src = this.photo.getThumbnailPath();
this.available = true;
if (this.onLoad) this.onLoad(); if (this.onLoad) this.onLoad();
} else if (this.photo.isReplacementThumbnailAvailable()) {
this.src = this.photo.getReplacementThumbnailPath();
this.available = true; this.available = true;
} this.loading = false;
this.thumbnailTask = null;
if (!this.photo.isThumbnailAvailable()) { },
setImmediate(() => { onError: (error) => {//onError
this.thumbnailTask = null;
let listener: ThumbnailLoadingListener = { //TODO: handle error
onStartedLoading: () => { //onLoadStarted //TODO: not an error if its from cache
this.loading = true; console.error("something bad happened");
}, console.error(error);
onLoad: () => {//onLoaded }
this.src = this.photo.getThumbnailPath(); };
if (this.onLoad) this.onLoad(); if (this.photo.isReplacementThumbnailAvailable()) {
this.available = true; this.thumbnailTask = this.thumbnailService.loadImage(this.photo, ThumbnailLoadingPriority.medium, listener);
this.loading = false;
this.thumbnailTask = null;
},
onError: (error) => {//onError
this.thumbnailTask = null;
//TODO: handle error
//TODO: not an error if its from cache
console.error("something bad happened");
console.error(error);
}
};
if (this.photo.isReplacementThumbnailAvailable()) {
this.thumbnailTask = this.thumbnailService.loadImage(this.photo, ThumbnailLoadingPriority.medium, listener);
} else {
this.thumbnailTask = this.thumbnailService.loadImage(this.photo, ThumbnailLoadingPriority.high, listener);
}
});
}
}
set Visible(visible: boolean) {
if (!this.thumbnailTask) return;
if (visible === true) {
if (this.photo.isReplacementThumbnailAvailable()) {
this.thumbnailTask.priority = ThumbnailLoadingPriority.medium;
} else {
this.thumbnailTask.priority = ThumbnailLoadingPriority.high;
}
} else { } else {
if (this.photo.isReplacementThumbnailAvailable()) { this.thumbnailTask = this.thumbnailService.loadImage(this.photo, ThumbnailLoadingPriority.high, listener);
this.thumbnailTask.priority = ThumbnailLoadingPriority.low;
} else {
this.thumbnailTask.priority = ThumbnailLoadingPriority.medium;
}
} }
});
} }
}
set Visible(visible: boolean) {
if (!this.thumbnailTask) return;
if (visible === true) {
if (this.photo.isReplacementThumbnailAvailable()) {
this.thumbnailTask.priority = ThumbnailLoadingPriority.medium;
} else {
this.thumbnailTask.priority = ThumbnailLoadingPriority.high;
}
} else {
if (this.photo.isReplacementThumbnailAvailable()) {
this.thumbnailTask.priority = ThumbnailLoadingPriority.low;
} else {
this.thumbnailTask.priority = ThumbnailLoadingPriority.medium;
}
}
}
} }

View File

@@ -7,33 +7,33 @@ import {UserDTO} from "../../../common/entities/UserDTO";
import {ErrorCodes} from "../../../common/entities/Error"; import {ErrorCodes} from "../../../common/entities/Error";
@Component({ @Component({
selector: 'login', selector: 'login',
templateUrl: 'app/login/login.component.html', templateUrl: './login.component.html',
styleUrls: ['app/login/login.component.css'], styleUrls: ['./login.component.css'],
}) })
export class LoginComponent implements OnInit { export class LoginComponent implements OnInit {
loginCredential: LoginCredential; loginCredential: LoginCredential;
loginError: any = null; loginError: any = null;
constructor(private _authService: AuthenticationService, private _router: Router) { constructor(private _authService: AuthenticationService, private _router: Router) {
this.loginCredential = new LoginCredential(); this.loginCredential = new LoginCredential();
}
ngOnInit() {
if (this._authService.isAuthenticated()) {
this._router.navigate(['gallery', "/"]);
} }
}
ngOnInit() { onLogin() {
if (this._authService.isAuthenticated()) { this.loginError = null;
this._router.navigate(['gallery', "/"]); this._authService.login(this.loginCredential).then((message: Message<UserDTO>) => {
if (message.error) {
if (message.error.code === ErrorCodes.CREDENTIAL_NOT_FOUND) {
this.loginError = "Wrong username or password";
} }
} }
});
onLogin() { }
this.loginError = null;
this._authService.login(this.loginCredential).then((message: Message<UserDTO>) => {
if (message.error) {
if (message.error.code === ErrorCodes.CREDENTIAL_NOT_FOUND) {
this.loginError = "Wrong username or password";
}
}
});
}
} }

View File

@@ -1,10 +1,10 @@
export interface IRenderable { export interface IRenderable {
getDimension():Dimension; getDimension(): Dimension;
} }
export interface Dimension { export interface Dimension {
top: number; top: number;
left: number; left: number;
width: number; width: number;
height: number; height: number;
} }

View File

@@ -7,45 +7,49 @@ import {LoginCredential} from "../../../../common/entities/LoginCredential";
import {AuthenticationService} from "./authentication.service"; import {AuthenticationService} from "./authentication.service";
class MockUserService { class MockUserService {
public login(credential: LoginCredential) { public login(credential: LoginCredential) {
return Promise.resolve(new Message<UserDTO>(null, <UserDTO>{name: "testUserName"})) return Promise.resolve(new Message<UserDTO>(null, <UserDTO>{name: "testUserName"}))
} }
public async getSessionUser() {
return null;
}
} }
describe('AuthenticationService', () => { describe('AuthenticationService', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
{provide: UserService, useClass: MockUserService}, {provide: UserService, useClass: MockUserService},
AuthenticationService] AuthenticationService]
}); });
});
it('should call UserDTO service login', inject([AuthenticationService, UserService], (authService, userService) => {
spyOn(userService, "login").and.callThrough();
expect(userService.login).not.toHaveBeenCalled();
authService.login();
expect(userService.login).toHaveBeenCalled();
}));
it('should have NO Authenticated use', inject([AuthenticationService], (authService) => {
expect(authService.getUser()).toBe(null);
expect(authService.isAuthenticated()).toBe(false);
}));
it('should have Authenticated use', inject([AuthenticationService], (authService) => {
spyOn(authService.OnUserChanged, "trigger").and.callThrough();
authService.login();
authService.OnUserChanged.on(() => {
expect(authService.OnUserChanged.trigger).toHaveBeenCalled();
expect(authService.getUser()).not.toBe(null);
expect(authService.isAuthenticated()).toBe(true);
}); });
}));
it('should call UserDTO service login', inject([AuthenticationService, UserService], (authService, userService) => {
spyOn(userService, "login").and.callThrough();
expect(userService.login).not.toHaveBeenCalled();
authService.login();
expect(userService.login).toHaveBeenCalled();
}));
it('should have NO Authenticated use', inject([AuthenticationService], (authService) => {
expect(authService.getUser()).toBe(null);
expect(authService.isAuthenticated()).toBe(false);
}));
it('should have Authenticated use', inject([AuthenticationService], (authService) => {
spyOn(authService.OnUserChanged, "trigger").and.callThrough();
authService.login();
authService.OnUserChanged.on(() => {
expect(authService.OnUserChanged.trigger).toHaveBeenCalled();
expect(authService.getUser()).not.toBe(null);
expect(authService.isAuthenticated()).toBe(true);
});
}));
}); });

View File

@@ -9,76 +9,76 @@ import {ErrorCodes} from "../../../../common/entities/Error";
import {Config} from "../../../../common/config/public/Config"; import {Config} from "../../../../common/config/public/Config";
declare module ServerInject { declare module ServerInject {
export let user: UserDTO; export let user: UserDTO;
} }
@Injectable() @Injectable()
export class AuthenticationService { export class AuthenticationService {
private _user: UserDTO = null; private _user: UserDTO = null;
public OnUserChanged: Event<UserDTO>; public OnUserChanged: Event<UserDTO>;
constructor(private _userService: UserService) { constructor(private _userService: UserService) {
this.OnUserChanged = new Event(); this.OnUserChanged = new Event();
//picking up session..
if (this.isAuthenticated() == false && Cookie.get('pigallery2-session') != null) {
if (typeof ServerInject !== "undefined" && typeof ServerInject.user !== "undefined") {
this.setUser(ServerInject.user);
}
this.getSessionUser();
} else {
this.OnUserChanged.trigger(this._user);
}
//picking up session..
if (this.isAuthenticated() == false && Cookie.get('pigallery2-session') != null) {
if (typeof ServerInject !== "undefined" && typeof ServerInject.user !== "undefined") {
this.setUser(ServerInject.user);
}
this.getSessionUser();
} else {
this.OnUserChanged.trigger(this._user);
} }
private getSessionUser() { }
this._userService.getSessionUser().then((message: Message<UserDTO>) => {
if (message.error) {
console.log(message.error);
} else {
this._user = message.result;
this.OnUserChanged.trigger(this._user);
}
});
}
private setUser(user: UserDTO) { private getSessionUser() {
this._user = user; this._userService.getSessionUser().then((message: Message<UserDTO>) => {
if (message.error) {
console.log(message.error);
} else {
this._user = message.result;
this.OnUserChanged.trigger(this._user); this.OnUserChanged.trigger(this._user);
} }
});
}
public login(credential: LoginCredential) { private setUser(user: UserDTO) {
return this._userService.login(credential).then((message: Message<UserDTO>) => { this._user = user;
if (message.error) { this.OnUserChanged.trigger(this._user);
console.log(ErrorCodes[message.error.code] + ", message: ", message.error.message); }
} else {
this.setUser(message.result); public login(credential: LoginCredential) {
} return this._userService.login(credential).then((message: Message<UserDTO>) => {
return message; if (message.error) {
}); console.log(ErrorCodes[message.error.code] + ", message: ", message.error.message);
} } else {
this.setUser(message.result);
}
return message;
});
}
public isAuthenticated(): boolean { public isAuthenticated(): boolean {
if (Config.Client.authenticationRequired === false) { if (Config.Client.authenticationRequired === false) {
return true; return true;
}
return !!(this._user && this._user != null);
} }
return !!(this._user && this._user != null);
}
public getUser() { public getUser() {
if (Config.Client.authenticationRequired === false) { if (Config.Client.authenticationRequired === false) {
return <UserDTO>{name: "", password: "", role: UserRoles.Admin}; return <UserDTO>{name: "", password: "", role: UserRoles.Admin};
}
return this._user;
} }
return this._user;
}
public logout() { public logout() {
this._userService.logout(); this._userService.logout();
this.setUser(null); this.setUser(null);
} }
} }

View File

@@ -7,169 +7,169 @@ import {Message} from "../../../../common/entities/Message";
describe('NetworkService Success tests', () => { describe('NetworkService Success tests', () => {
let connection: MockConnection = null; let connection: MockConnection = null;
let testUrl = "/test/url"; let testUrl = "/test/url";
let testData = {data: "testData"}; let testData = {data: "testData"};
let testResponse = "testResponse"; let testResponse = "testResponse";
let testResponseMessage = new Message(null, testResponse); let testResponseMessage = new Message(null, testResponse);
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
MockBackend, MockBackend,
BaseRequestOptions, BaseRequestOptions,
{ {
provide: Http, useFactory: (backend, options) => { provide: Http, useFactory: (backend, options) => {
return new Http(backend, options); return new Http(backend, options);
}, deps: [MockBackend, BaseRequestOptions] }, deps: [MockBackend, BaseRequestOptions]
}, },
NetworkService NetworkService
] ]
}); });
});
beforeEach(inject([MockBackend], (backend) => {
backend.connections.subscribe((c) => {
connection = c;
connection.mockRespond(new Response(
new ResponseOptions(
{
body: testResponseMessage
}
)));
});
}));
afterEach(() => {
expect(connection.request.url).toBe("/api" + testUrl);
});
it('should call GET', inject([NetworkService], (networkService) => {
networkService.getJson(testUrl).then((res: Message<any>) => {
expect(res.result).toBe(testResponse);
}); });
}));
beforeEach(inject([MockBackend], (backend) => { it('should call POST', inject([NetworkService, MockBackend], (networkService) => {
backend.connections.subscribe((c) => { networkService.postJson(testUrl, testData).then((res: Message<any>) => {
connection = c; expect(res.result).toBe(testResponse);
connection.mockRespond(new Response( });
new ResponseOptions( expect(connection.request.text()).toBe(JSON.stringify(testData));
{
body: testResponseMessage
}
)));
});
}));
afterEach(() => {
expect(connection.request.url).toBe("/api" + testUrl); networkService.postJson(testUrl).then((res: Message<any>) => {
expect(res.result).toBe(testResponse);
});
expect(connection.request.text()).toBe(JSON.stringify({}));
}));
it('should call PUT', inject([NetworkService, MockBackend], (networkService) => {
networkService.putJson(testUrl, testData).then((res: Message<any>) => {
expect(res.result).toBe(testResponse);
}); });
it('should call GET', inject([NetworkService], (networkService) => { expect(connection.request.text()).toBe(JSON.stringify(testData));
networkService.getJson(testUrl).then((res: Message<any>) => {
expect(res.result).toBe(testResponse);
});
}));
it('should call POST', inject([NetworkService, MockBackend], (networkService) => {
networkService.postJson(testUrl, testData).then((res: Message<any>) => {
expect(res.result).toBe(testResponse);
});
expect(connection.request.text()).toBe(JSON.stringify(testData));
networkService.postJson(testUrl).then((res: Message<any>) => { networkService.putJson(testUrl).then((res: Message<any>) => {
expect(res.result).toBe(testResponse); expect(res.result).toBe(testResponse);
}); });
expect(connection.request.text()).toBe(JSON.stringify({})); expect(connection.request.text()).toBe(JSON.stringify({}));
}));
it('should call PUT', inject([NetworkService, MockBackend], (networkService) => { }));
networkService.putJson(testUrl, testData).then((res: Message<any>) => { it('should call DELETE', inject([NetworkService, MockBackend], (networkService) => {
expect(res.result).toBe(testResponse);
});
expect(connection.request.text()).toBe(JSON.stringify(testData)); networkService.deleteJson(testUrl).then((res: Message<any>) => {
expect(res.result).toBe(testResponse);
});
networkService.putJson(testUrl).then((res: Message<any>) => { }));
expect(res.result).toBe(testResponse);
});
expect(connection.request.text()).toBe(JSON.stringify({}));
}));
it('should call DELETE', inject([NetworkService, MockBackend], (networkService) => {
networkService.deleteJson(testUrl).then((res: Message<any>) => {
expect(res.result).toBe(testResponse);
});
}));
}); });
describe('NetworkService Fail tests', () => { describe('NetworkService Fail tests', () => {
let connection: MockConnection = null; let connection: MockConnection = null;
let testUrl = "/test/url"; let testUrl = "/test/url";
let testData = {data: "testData"}; let testData = {data: "testData"};
let testError = "testError"; let testError = "testError";
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
MockBackend, MockBackend,
BaseRequestOptions, BaseRequestOptions,
{ {
provide: Http, useFactory: (backend, options) => { provide: Http, useFactory: (backend, options) => {
return new Http(backend, options); return new Http(backend, options);
}, deps: [MockBackend, BaseRequestOptions] }, deps: [MockBackend, BaseRequestOptions]
}, },
NetworkService NetworkService
] ]
}); });
});
beforeEach(inject([MockBackend], (backend) => {
backend.connections.subscribe((c) => {
connection = c;
connection.mockError({name: "errorName", message: testError});
});
}));
afterEach(() => {
expect(connection.request.url).toBe("/api" + testUrl);
});
it('should call GET with error', inject([NetworkService], (networkService) => {
networkService.getJson(testUrl).then((res: Message<any>) => {
expect(res).toBe(null);
}).catch((err) => {
expect(err).toBe(testError);
}); });
beforeEach(inject([MockBackend], (backend) => { }));
backend.connections.subscribe((c) => { it('should call POST with error', inject([NetworkService, MockBackend], (networkService) => {
connection = c;
connection.mockError({name: "errorName", message: testError});
}); networkService.postJson(testUrl, testData).then((res: Message<any>) => {
})); expect(res).toBe(null);
}).catch((err) => {
expect(err).toBe(testError);
});
expect(connection.request.text()).toBe(JSON.stringify(testData));
}));
afterEach(() => { it('should call PUT with error', inject([NetworkService, MockBackend], (networkService) => {
expect(connection.request.url).toBe("/api" + testUrl); networkService.putJson(testUrl, testData).then((res: Message<any>) => {
expect(res).toBe(null);
}).catch((err) => {
expect(err).toBe(testError);
}); });
it('should call GET with error', inject([NetworkService], (networkService) => { expect(connection.request.text()).toBe(JSON.stringify(testData));
networkService.getJson(testUrl).then((res: Message<any>) => { }));
expect(res).toBe(null);
}).catch((err) => {
expect(err).toBe(testError);
});
})); it('should call DELETE with error', inject([NetworkService, MockBackend], (networkService) => {
it('should call POST with error', inject([NetworkService, MockBackend], (networkService) => { networkService.deleteJson(testUrl).then((res: Message<any>) => {
expect(res).toBe(null);
networkService.postJson(testUrl, testData).then((res: Message<any>) => { }).catch((err) => {
expect(res).toBe(null); expect(err).toBe(testError);
}).catch((err) => { });
expect(err).toBe(testError); }));
});
expect(connection.request.text()).toBe(JSON.stringify(testData));
}));
it('should call PUT with error', inject([NetworkService, MockBackend], (networkService) => {
networkService.putJson(testUrl, testData).then((res: Message<any>) => {
expect(res).toBe(null);
}).catch((err) => {
expect(err).toBe(testError);
});
expect(connection.request.text()).toBe(JSON.stringify(testData));
}));
it('should call DELETE with error', inject([NetworkService, MockBackend], (networkService) => {
networkService.deleteJson(testUrl).then((res: Message<any>) => {
expect(res).toBe(null);
}).catch((err) => {
expect(err).toBe(testError);
});
}));
}); });

View File

@@ -1,55 +1,55 @@
import {Injectable} from "@angular/core"; import {Injectable} from "@angular/core";
import {Http, Headers, RequestOptions} from "@angular/http"; import {Headers, Http, RequestOptions} from "@angular/http";
import {Message} from "../../../../common/entities/Message"; import {Message} from "../../../../common/entities/Message";
import "rxjs/Rx"; import "rxjs/Rx";
@Injectable() @Injectable()
export class NetworkService { export class NetworkService {
_baseUrl = "/api"; _baseUrl = "/api";
constructor(protected _http:Http) { constructor(protected _http: Http) {
}
private callJson<T>(method: string, url: string, data: any = {}): Promise<T> {
let body = JSON.stringify(data);
let headers = new Headers({'Content-Type': 'application/json'});
let options = new RequestOptions({headers: headers});
if (method == "get" || method == "delete") {
return <any>this._http[method](this._baseUrl + url, options)
.toPromise()
.then(res => <Message<any>> res.json())
.catch(NetworkService.handleError);
} }
private callJson<T>(method:string, url:string, data:any = {}):Promise<T> { return this._http[method](this._baseUrl + url, body, options)
let body = JSON.stringify(data); .toPromise()
let headers = new Headers({'Content-Type': 'application/json'}); .then((res: any) => <Message<any>> res.json())
let options = new RequestOptions({headers: headers}); .catch(NetworkService.handleError);
}
if (method == "get" || method == "delete") { public postJson<T>(url: string, data: any = {}): Promise<T> {
return <any>this._http[method](this._baseUrl + url, options) return this.callJson("post", url, data);
.toPromise() }
.then(res => <Message<any>> res.json())
.catch(NetworkService.handleError);
}
return this._http[method](this._baseUrl + url, body, options) public putJson<T>(url: string, data: any = {}): Promise<T> {
.toPromise() return this.callJson("put", url, data);
.then((res: any) => <Message<any>> res.json()) }
.catch(NetworkService.handleError);
}
public postJson<T>(url:string, data:any = {}):Promise<T> { public getJson<T>(url: string): Promise<T> {
return this.callJson("post", url, data); return this.callJson("get", url);
} }
public putJson<T>(url:string, data:any = {}):Promise<T> {
return this.callJson("put", url, data);
}
public getJson<T>(url:string):Promise<T> {
return this.callJson("get", url);
}
public deleteJson<T>(url:string):Promise<T> { public deleteJson<T>(url: string): Promise<T> {
return this.callJson("delete", url); return this.callJson("delete", url);
} }
private static handleError(error:any) { private static handleError(error: any) {
// TODO: in a real world app do smthing better // TODO: in a real world app do smthing better
// instead of just logging it to the console // instead of just logging it to the console
console.error(error); console.error(error);
return Promise.reject(error.message || error.json().error || 'Server error'); return Promise.reject(error.message || error.json().error || 'Server error');
} }
} }

View File

@@ -9,36 +9,36 @@ import {LoginCredential} from "../../../../common/entities/LoginCredential";
describe('UserService', () => { describe('UserService', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
MockBackend, MockBackend,
BaseRequestOptions, BaseRequestOptions,
{ {
provide: Http, useFactory: (backend, options) => { provide: Http, useFactory: (backend, options) => {
return new Http(backend, options); return new Http(backend, options);
}, deps: [MockBackend, BaseRequestOptions] }, deps: [MockBackend, BaseRequestOptions]
}, },
NetworkService, NetworkService,
UserService] UserService]
});
it('should call postJson at login', inject([UserService, NetworkService], (userService, networkService) => {
spyOn(networkService, "postJson");
let credential = new LoginCredential("name", "pass");
userService.login(credential);
expect(networkService.postJson).toHaveBeenCalled();
expect(networkService.postJson.calls.argsFor(0)).toEqual(["/user/login", {"loginCredential": credential}]);
}));
it('should call getJson at getSessionUser', inject([UserService, NetworkService], (userService, networkService) => {
spyOn(networkService, "getJson");
userService.getSessionUser();
expect(networkService.getJson).toHaveBeenCalled();
expect(networkService.getJson.calls.argsFor(0)).toEqual(["/user/login"]);
}));
}); });
});
it('should call postJson at login', inject([UserService, NetworkService], (userService, networkService) => {
spyOn(networkService, "postJson");
let credential = new LoginCredential("name", "pass");
userService.login(credential);
expect(networkService.postJson).toHaveBeenCalled();
expect(networkService.postJson.calls.argsFor(0)).toEqual(["/user/login", {"loginCredential": credential}]);
}));
it('should call getJson at getSessionUser', inject([UserService, NetworkService], (userService, networkService) => {
spyOn(networkService, "getJson");
userService.getSessionUser();
expect(networkService.getJson).toHaveBeenCalled();
expect(networkService.getJson.calls.argsFor(0)).toEqual(["/user/login"]);
}));
});
});

View File

@@ -8,20 +8,20 @@ import {Message} from "../../../../common/entities/Message";
export class UserService { export class UserService {
constructor(private _networkService: NetworkService) { constructor(private _networkService: NetworkService) {
} }
public logout(): Promise<Message<string>> { public logout(): Promise<Message<string>> {
console.log("call logout"); console.log("call logout");
return this._networkService.postJson("/user/logout"); return this._networkService.postJson("/user/logout");
} }
public login(credential: LoginCredential): Promise<Message<UserDTO>> { public login(credential: LoginCredential): Promise<Message<UserDTO>> {
return this._networkService.postJson("/user/login", {"loginCredential": credential}); return this._networkService.postJson("/user/login", {"loginCredential": credential});
} }
public getSessionUser(): Promise<Message<UserDTO>> { public getSessionUser(): Promise<Message<UserDTO>> {
return this._networkService.getJson("/user/login"); return this._networkService.getJson("/user/login");
} }
} }

View File

@@ -4,24 +4,24 @@ import {Injectable} from "@angular/core";
export class NotificationService { export class NotificationService {
constructor() { constructor() {
} }
public showException(message:string) { public showException(message: string) {
} }
public showError(message:string) { public showError(message: string) {
} }
public showWarn(message:string) { public showWarn(message: string) {
} }
public showInfo(message:string) { public showInfo(message: string) {
} }
} }

View File

@@ -4,8 +4,8 @@ import {UserRoles} from "../../../common/entities/UserDTO";
@Pipe({name: 'stringifyRole'}) @Pipe({name: 'stringifyRole'})
export class StringifyRole implements PipeTransform { export class StringifyRole implements PipeTransform {
transform(role:string):number { transform(role: string): number {
return UserRoles[role]; return UserRoles[role];
} }
} }

View File

@@ -49,7 +49,8 @@
aria-hidden="true">&times;</span></button> aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="myModalLabel">Modal title</h4> <h4 class="modal-title" id="myModalLabel">Modal title</h4>
</div> </div>
<form (ngSubmit)="onSubmit()" #NewUserForm="ngForm">
<form #NewUserForm="ngForm">
<div class="modal-body"> <div class="modal-body">
<input type="text" class="form-control" placeholder="Username" autofocus <input type="text" class="form-control" placeholder="Username" autofocus
[(ngModel)]="newUser.name" name="name" required> [(ngModel)]="newUser.name" name="name" required>

View File

@@ -7,66 +7,66 @@ import {Message} from "../../../../common/entities/Message";
import {UserManagerSettingsService} from "./usermanager.settings.service"; import {UserManagerSettingsService} from "./usermanager.settings.service";
@Component({ @Component({
selector: 'settings-usermanager', selector: 'settings-usermanager',
templateUrl: 'app/settings/usermanager/usermanager.settings.component.html', templateUrl: './usermanager.settings.component.html',
styleUrls: ['app/settings/usermanager/usermanager.settings.component.css'], styleUrls: ['./usermanager.settings.component.css'],
providers: [UserManagerSettingsService], providers: [UserManagerSettingsService],
}) })
export class UserMangerSettingsComponent implements OnInit { export class UserMangerSettingsComponent implements OnInit {
private newUser = <UserDTO>{}; public newUser = <UserDTO>{};
private userRoles: Array<any> = []; public userRoles: Array<any> = [];
private users: Array<UserDTO> = []; public users: Array<UserDTO> = [];
constructor(private _authService: AuthenticationService, private _router: Router, private _userSettings: UserManagerSettingsService) { constructor(private _authService: AuthenticationService, private _router: Router, private _userSettings: UserManagerSettingsService) {
}
ngOnInit() {
if (!this._authService.isAuthenticated() || this._authService.getUser().role < UserRoles.Admin) {
this._router.navigate(['login']);
return;
}
this.userRoles = Utils.enumToArray(UserRoles).filter(r => r.key <= this._authService.getUser().role);
this.getUsersList();
}
private getUsersList() {
this._userSettings.getUsers().then((result: Message<Array<UserDTO>>) => {
this.users = result.result;
});
}
canModifyUser(user: UserDTO): boolean {
let currentUser = this._authService.getUser();
if (!currentUser) {
return false;
} }
ngOnInit() { return currentUser.name != user.name && currentUser.role >= user.role;
if (!this._authService.isAuthenticated() || this._authService.getUser().role < UserRoles.Admin) { }
this._router.navigate(['login']);
return;
}
this.userRoles = Utils.enumToArray(UserRoles).filter(r => r.key <= this._authService.getUser().role);
this.getUsersList();
}
private getUsersList() { initNewUser() {
this._userSettings.getUsers().then((result: Message<Array<UserDTO>>) => { this.newUser = <UserDTO>{role: UserRoles.User};
this.users = result.result; }
});
}
addNewUser() {
this._userSettings.createUser(this.newUser).then(() => {
this.getUsersList();
});
}
canModifyUser(user: UserDTO): boolean { updateRole(user: UserDTO) {
let currentUser = this._authService.getUser(); this._userSettings.updateRole(user).then(() => {
if (!currentUser) { this.getUsersList();
return false; });
} }
return currentUser.name != user.name && currentUser.role >= user.role; deleteUser(user: UserDTO) {
} this._userSettings.deleteUser(user).then(() => {
this.getUsersList();
initNewUser() { });
this.newUser = <UserDTO>{role: UserRoles.User}; }
}
addNewUser() {
this._userSettings.createUser(this.newUser).then(() => {
this.getUsersList();
});
}
updateRole(user: UserDTO) {
this._userSettings.updateRole(user).then(() => {
this.getUsersList();
});
}
deleteUser(user: UserDTO) {
this._userSettings.deleteUser(user).then(() => {
this.getUsersList();
});
}
} }

View File

@@ -7,24 +7,24 @@ import {Message} from "../../../../common/entities/Message";
export class UserManagerSettingsService { export class UserManagerSettingsService {
constructor(private _networkService:NetworkService) { constructor(private _networkService: NetworkService) {
} }
public createUser(user: UserDTO): Promise<Message<string>> { public createUser(user: UserDTO): Promise<Message<string>> {
return this._networkService.putJson("/user", {newUser: user}); return this._networkService.putJson("/user", {newUser: user});
} }
public getUsers(): Promise<Message<Array<UserDTO>>> { public getUsers(): Promise<Message<Array<UserDTO>>> {
return this._networkService.getJson("/user/list"); return this._networkService.getJson("/user/list");
} }
public deleteUser(user: UserDTO) { public deleteUser(user: UserDTO) {
return this._networkService.deleteJson("/user/" + user.id); return this._networkService.deleteJson("/user/" + user.id);
} }
public updateRole(user: UserDTO) { public updateRole(user: UserDTO) {
return this._networkService.postJson("/user/" + user.id + "/role", {newRole: user.role}); return this._networkService.postJson("/user/" + user.id + "/role", {newRole: user.role});
} }
} }

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@@ -0,0 +1,4 @@
var ServerInject = {
user: <%- JSON.stringify(user); %>,
ConfigInject: <%- JSON.stringify(clientConfig); %>
}

View File

@@ -0,0 +1,3 @@
export const environment = {
production: true
};

View File

@@ -0,0 +1,8 @@
// The file contents for the current environment will overwrite these during build.
// The build system defaults to the dev environment which uses `environment.ts`, but if you do
// `ng build --env=prod` then `environment.prod.ts` will be used instead.
// The list of which env maps to which file can be found in `.angular-cli.json`.
export const environment = {
production: false
};

View File

@@ -1,48 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="/"/>
<meta charset="UTF-8">
<title>PiGallery2</title>
<link rel="shortcut icon" href="icon.png">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7"
crossorigin="anonymous">
<!-- Polyfill(s) for older browsers -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/reflect-metadata/Reflect.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script>
var ServerInject = {
user: <%- JSON.stringify(user); %>,
ConfigInject: <%- JSON.stringify(clientConfig); %>
}
</script>
<script src="systemjs.config.js"></script>
<script>
System.import('').catch(function (err) {
console.error(err);
});
</script>
</head>
<body style="overflow-y: scroll">
<pi-gallery2-app>Loading...</pi-gallery2-app>
<script
src="https://code.jquery.com/jquery-2.2.3.min.js"
integrity="sha256-a23g1Nt4dtEYOj7bR+vTu7+T8VP13humZFBJNIYoEJo="
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"
integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS"
crossorigin="anonymous"></script>
</body>
</html>

28
frontend/index.html Normal file
View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<base href="/"/>
<meta charset="UTF-8">
<title>PiGallery2</title>
<link rel="shortcut icon" href="assets/icon.png">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7"
crossorigin="anonymous">
<script type="text/javascript" src="config_inject.js"></script>
</head>
<body style="overflow-y: scroll">
<pi-gallery2-app>Loading...</pi-gallery2-app>
<script
src="https://code.jquery.com/jquery-2.2.3.min.js"
integrity="sha256-a23g1Nt4dtEYOj7bR+vTu7+T8VP13humZFBJNIYoEJo="
crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"
integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS"
crossorigin="anonymous"></script>
</body>
</html>

61
frontend/polyfills.ts Normal file
View File

@@ -0,0 +1,61 @@
/**
* This file includes polyfills needed by Angular and is loaded before the app.
* You can add your own extra polyfills to this file.
*
* This file is divided into 2 sections:
* 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers.
* 2. Application imports. Files imported after ZoneJS that should be loaded before your main
* file.
*
* The current setup is for so-called "evergreen" browsers; the last versions of browsers that
* automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera),
* Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile.
*
* Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html
*/
/***************************************************************************************************
* BROWSER POLYFILLS
*/
/** IE9, IE10 and IE11 requires all of the following polyfills. **/
// import 'core-js/es6/symbol';
import 'core-js/es6/object';
// import 'core-js/es6/function';
// import 'core-js/es6/parse-int';
// import 'core-js/es6/parse-float';
// import 'core-js/es6/number';
// import 'core-js/es6/math';
// import 'core-js/es6/string';
// import 'core-js/es6/date';
import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/** IE10 and IE11 requires the following to support `@angular/animation`. */
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/** Evergreen browsers require these. **/
import "core-js/es6/reflect";
import "core-js/es7/reflect";
/** ALL Firefox browsers require the following to support `@angular/animation`. **/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/***************************************************************************************************
* Zone JS is required by Angular itself.
*/
import "zone.js/dist/zone"; // Included with Angular CLI.
/***************************************************************************************************
* APPLICATION IMPORTS
*/
/**
* Date, currency, decimal and percent pipes.
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
*/
import 'intl'; // Run `npm install --save intl`.
/**
* Need to import at least one locale-data with intl.
*/
// import 'intl/locale-data/jsonp/en';

1
frontend/styles.css Normal file
View File

@@ -0,0 +1 @@
/* You can add global styles to this file, and also import other style files */

View File

@@ -1,11 +0,0 @@
/**
* Add barrels and stuff
* Adjust as necessary for your application needs.
*/
// (function (global) {
// System.config({
// packages: {
// // add packages here
// }
// });
// })(this);

View File

@@ -1,48 +0,0 @@
/**
* System configuration for Angular samples
* Adjust as necessary for your application needs.
*/
(function (global) {
System.config({
paths: {
// paths serve as alias
'npm:': 'node_modules/'
},
// map tells the System loader where to look for things
map: {
// our app is within the app folder
app: '',
// angular bundles
'@angular/core': 'npm:@angular/core/bundles/core.umd.js',
'@angular/common': 'npm:@angular/common/bundles/common.umd.js',
'@angular/compiler': 'npm:@angular/compiler/bundles/compiler.umd.js',
'@angular/platform-browser': 'npm:@angular/platform-browser/bundles/platform-browser.umd.js',
'@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic.umd.js',
'@angular/http': 'npm:@angular/http/bundles/http.umd.js',
'@angular/router': 'npm:@angular/router/bundles/router.umd.js',
'@angular/forms': 'npm:@angular/forms/bundles/forms.umd.js',
// other libraries
'rxjs': 'npm:rxjs',
'@agm/core': 'npm:@agm/core/core.umd.js',
'ng2-cookies': 'npm:ng2-cookies/ng2-cookies',
'typeconfig': 'npm:typeconfig'
},
// packages tells the System loader how to load when no filename and/or no extension
packages: {
app: {
main: './main.js',
defaultExtension: 'js'
},
rxjs: {
defaultExtension: 'js'
},
"angular2-google-maps/core": {
"defaultExtension": "js",
"main": "index.js"
}
}
});
})(this);

30
frontend/test.ts Normal file
View File

@@ -0,0 +1,30 @@
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
import "zone.js/dist/long-stack-trace-zone";
import "zone.js/dist/proxy.js";
import "zone.js/dist/sync-test";
import "zone.js/dist/jasmine-patch";
import "zone.js/dist/async-test";
import "zone.js/dist/fake-async-test";
import {getTestBed} from "@angular/core/testing";
import {BrowserDynamicTestingModule, platformBrowserDynamicTesting} from "@angular/platform-browser-dynamic/testing";
// Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
declare const __karma__: any;
declare const require: any;
// Prevent Karma from running prematurely.
__karma__.loaded = function () {
};
// First, initialize the Angular testing environment.
getTestBed().initTestEnvironment(
BrowserDynamicTestingModule,
platformBrowserDynamicTesting()
);
// Then we find all the tests.
const context = require.context('./', true, /\.spec\.ts$/);
// And load the modules.
context.keys().map(context);
// Finally, start Karma to run the tests.
__karma__.start();

View File

@@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/app",
"module": "es2015",
"baseUrl": "",
"types": []
},
"exclude": [
"test.ts",
"**/*.spec.ts"
]
}

View File

@@ -0,0 +1,20 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/spec",
"module": "commonjs",
"target": "es5",
"baseUrl": "",
"types": [
"jasmine",
"node"
]
},
"files": [
"test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
]
}

5
frontend/typings.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/* SystemJS module definition */
declare var module: NodeModule;
interface NodeModule {
id: string;
}

View File

@@ -1,98 +0,0 @@
// /*global jasmine, __karma__, window*/
Error.stackTraceLimit = 0; // "No stacktrace"" is usually best for app testing.
// Uncomment to get full stacktrace output. Sometimes helpful, usually not.
// Error.stackTraceLimit = Infinity; //
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
// builtPaths: root paths for output ("built") files
// get from karma.config.js, then prefix with '/base/' (default is 'app/')
var builtPaths = (__karma__.config.builtPaths || ['app/'])
.map(function (p) {
return '/base/' + p;
});
__karma__.loaded = function () {
};
function isJsFile(path) {
return path.slice(-3) == '.js';
}
function isSpecFile(path) {
return /\.spec\.(.*\.)?js$/.test(path);
}
// Is a "built" file if is JavaScript file in one of the "built" folders
function isBuiltFile(path) {
return isJsFile(path) &&
builtPaths.reduce(function (keep, bp) {
return keep || (path.substr(0, bp.length) === bp);
}, false);
}
var allSpecFiles = Object.keys(window.__karma__.files)
.filter(isSpecFile)
.filter(isBuiltFile);
System.config({
baseURL: 'base',
// Extend usual application package list with test folder
packages: {'testing': {main: 'index.js', defaultExtension: 'js'}},
// Assume npm: is set in `paths` in systemjs.config
// Map the angular testing umd bundles
map: {
'@angular/core/testing': 'npm:@angular/core/bundles/core-testing.umd.js',
'@angular/common/testing': 'npm:@angular/common/bundles/common-testing.umd.js',
'@angular/compiler/testing': 'npm:@angular/compiler/bundles/compiler-testing.umd.js',
'@angular/platform-browser/testing': 'npm:@angular/platform-browser/bundles/platform-browser-testing.umd.js',
'@angular/platform-browser-dynamic/testing': 'npm:@angular/platform-browser-dynamic/bundles/platform-browser-dynamic-testing.umd.js',
'@angular/http/testing': 'npm:@angular/http/bundles/http-testing.umd.js',
'@angular/router/testing': 'npm:@angular/router/bundles/router-testing.umd.js',
'@angular/forms/testing': 'npm:@angular/forms/bundles/forms-testing.umd.js',
},
});
System.import('frontend/systemjs.config.js')
.then(importSystemJsExtras)
.then(initTestBed)
.then(initTesting);
/** Optional SystemJS configuration extras. Keep going w/o it */
function importSystemJsExtras() {
return System.import('frontend/systemjs.config.extras.js')
.catch(function (reason) {
console.log(
'Warning: System.import could not load the optional "systemjs.config.extras.js". Did you omit it by accident? Continuing without it.'
);
console.log(reason);
});
}
function initTestBed() {
return Promise.all([
System.import('@angular/core/testing'),
System.import('@angular/platform-browser-dynamic/testing')
])
.then(function (providers) {
var coreTesting = providers[0];
var browserTesting = providers[1];
coreTesting.TestBed.initTestEnvironment(
browserTesting.BrowserDynamicTestingModule,
browserTesting.platformBrowserDynamicTesting());
})
}
// Import all spec files and start karma
function initTesting() {
return Promise.all(
allSpecFiles.map(function (moduleName) {
return System.import(moduleName);
})
)
.then(__karma__.start, __karma__.error);
}

View File

@@ -1,106 +1,33 @@
module.exports = function(config) { // Karma configuration file, see link for more information
// https://karma-runner.github.io/0.13/config/configuration-file.html
var appBase = 'frontend/'; // transpiled app JS and map files module.exports = function (config) {
var appSrcBase = 'frontend/'; // app source TS files config.set({
var commonBase = 'common/'; // transpiled app JS and map files basePath: '',
var commonSrcBase = 'common/'; // app source TS files frameworks: ['jasmine', '@angular/cli'],
plugins: [
var appAssets = 'base/'; // component assets fetched by Angular's compiler require('karma-jasmine'),
require('karma-phantomjs-launcher'),
// Testing helpers (optional) are conventionally in a folder called `testing` require('karma-jasmine-html-reporter'),
var testingBase = 'testing/'; // transpiled test JS and map files require('karma-coverage-istanbul-reporter'),
var testingSrcBase = 'testing/'; // test source TS files require('@angular/cli/plugins/karma')
],
config.set({ client:{
basePath: '', clearContext: false // leave Jasmine Spec Runner output visible in browser
frameworks: ['jasmine'], },
coverageIstanbulReporter: {
plugins: [ reports: [ 'html', 'lcovonly' ],
require('karma-jasmine'), fixWebpackSourcePaths: true
require('karma-phantomjs-launcher'), },
require('karma-jasmine-html-reporter') angularCli: {
], environment: 'dev'
},
client: { reporters: ['progress', 'kjhtml'],
builtPaths: [appBase, commonBase, testingBase], // add more spec base paths as needed port: 9876,
clearContext: false // leave Jasmine Spec Runner output visible in browser colors: true,
}, logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['PhantomJS'],
files: [ singleRun: false
});
// Polyfills };
'node_modules/core-js/client/shim.js',
'node_modules/reflect-metadata/Reflect.js',
// System.js for module loading
'node_modules/systemjs/dist/system.js',
// zone.js
'node_modules/zone.js/dist/zone.js',
'node_modules/zone.js/dist/long-stack-trace-zone.js',
'node_modules/zone.js/dist/proxy.js',
'node_modules/zone.js/dist/sync-test.js',
'node_modules/zone.js/dist/jasmine-patch.js',
'node_modules/zone.js/dist/async-test.js',
'node_modules/zone.js/dist/fake-async-test.js',
// RxJs
{pattern: 'node_modules/rxjs/**/*.js', included: false, watched: false},
{pattern: 'node_modules/rxjs/**/*.js.map', included: false, watched: false},
//Other libs
{pattern: 'node_modules/ng2-cookies/**/*.js', included: false, watched: false},
{pattern: 'node_modules/typeconfig/**/*.js', included: false, watched: false},
// Paths loaded via module imports:
// Angular itself
{pattern: 'node_modules/@angular/**/*.js', included: false, watched: false},
{pattern: 'node_modules/@angular/**/*.js.map', included: false, watched: false},
{pattern: 'systemjs.config.js', included: false, watched: false},
{pattern: 'systemjs.config.extras.js', included: false, watched: false},
'karma-test-shim.js', // optionally extend SystemJS mapping e.g., with barrels
// transpiled application & spec code paths loaded via module imports
{pattern: appBase + '**/*.js', included: false, watched: true},
{pattern: commonBase + '**/*.js', included: false, watched: true},
{pattern: testingBase + '**/*.js', included: false, watched: true},
// Asset (HTML & CSS) paths loaded via Angular's component compiler
// (these paths need to be rewritten, see proxies section)
{pattern: appBase + '**/*.html', included: false, watched: true},
{pattern: appBase + '**/*.css', included: false, watched: true},
{pattern: commonBase + '**/*.html', included: false, watched: true},
{pattern: commonBase + '**/*.css', included: false, watched: true},
// Paths for debugging with source maps in dev tools
{pattern: appSrcBase + '**/*.ts', included: false, watched: false},
{pattern: commonSrcBase + '**/*.ts', included: false, watched: false},
{pattern: appBase + '**/*.js.map', included: false, watched: false},
{pattern: commonBase + '**/*.js.map', included: false, watched: false},
{pattern: testingSrcBase + '**/*.ts', included: false, watched: false},
{pattern: testingBase + '**/*.js.map', included: false, watched: false}
],
// Proxied base paths for loading assets
proxies: {
// required for component assets fetched by Angular's compiler
"/app/": appAssets
},
exclude: [],
preprocessors: {},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['PhantomJS'],
singleRun: false
})
}

View File

@@ -7,10 +7,12 @@
"license": "MIT", "license": "MIT",
"main": "./backend/server.js", "main": "./backend/server.js",
"scripts": { "scripts": {
"build": "tsc", "build": "ng build",
"pretest": "tsc", "test": "ng test --single-run && mocha --recursive test/backend/unit",
"test": "karma start karma.conf.js --single-run && mocha --recursive test/backend/unit", "start": "node ./backend/server",
"start": "node ./backend/server" "ng": "ng",
"lint": "ng lint",
"e2e": "ng e2e"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -37,6 +39,7 @@
"express-session": "^1.15.3", "express-session": "^1.15.3",
"express-winston": "^2.4.0", "express-winston": "^2.4.0",
"flat-file-db": "^1.0.0", "flat-file-db": "^1.0.0",
"intl": "^1.2.5",
"jimp": "^0.2.28", "jimp": "^0.2.28",
"mime": "^1.3.6", "mime": "^1.3.6",
"mocha": "^3.4.2", "mocha": "^3.4.2",
@@ -54,6 +57,9 @@
"zone.js": "^0.8.11" "zone.js": "^0.8.11"
}, },
"devDependencies": { "devDependencies": {
"@angular/cli": "1.1.1",
"@angular/compiler-cli": "^4.0.0",
"@angular/language-service": "^4.0.0",
"@types/express": "^4.0.35", "@types/express": "^4.0.35",
"@types/express-session": "1.15.0", "@types/express-session": "1.15.0",
"@types/jasmine": "^2.5.51", "@types/jasmine": "^2.5.51",
@@ -62,12 +68,16 @@
"@types/sharp": "^0.17.2", "@types/sharp": "^0.17.2",
"@types/winston": "^2.3.3", "@types/winston": "^2.3.3",
"chai": "^4.0.1", "chai": "^4.0.1",
"codelyzer": "~3.0.1",
"ejs-loader": "^0.3.0",
"gulp": "^3.9.1", "gulp": "^3.9.1",
"gulp-typescript": "^3.1.7", "gulp-typescript": "^3.1.7",
"gulp-zip": "^4.0.0", "gulp-zip": "^4.0.0",
"jasmine-core": "^2.6.2", "jasmine-core": "^2.6.2",
"jasmine-spec-reporter": "~4.1.0",
"karma": "^1.7.0", "karma": "^1.7.0",
"karma-cli": "^1.0.1", "karma-cli": "^1.0.1",
"karma-coverage-istanbul-reporter": "^1.2.1",
"karma-jasmine": "^1.1.0", "karma-jasmine": "^1.1.0",
"karma-jasmine-html-reporter": "^0.2.2", "karma-jasmine-html-reporter": "^0.2.2",
"karma-phantomjs-launcher": "^1.0.4", "karma-phantomjs-launcher": "^1.0.4",
@@ -81,6 +91,7 @@
"rimraf": "^2.6.1", "rimraf": "^2.6.1",
"run-sequence": "^1.2.2", "run-sequence": "^1.2.2",
"ts-helpers": "^1.1.2", "ts-helpers": "^1.1.2",
"ts-node": "~3.0.4",
"tslint": "^5.4.2", "tslint": "^5.4.2",
"typescript": "^2.3.4" "typescript": "^2.3.4"
}, },

28
protractor.conf.js Normal file
View File

@@ -0,0 +1,28 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./test/e2e/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: 'test/e2e/tsconfig.e2e.json'
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

Some files were not shown because too many files have changed in this diff Show More