1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-04-25 12:24:34 +02:00

implementing basic video support

This commit is contained in:
Patrik J. Braun 2018-11-04 19:28:32 +01:00
parent 56e570cf3c
commit f13f333d49
52 changed files with 764 additions and 500 deletions

View File

@ -59,8 +59,8 @@ export class GalleryMWs {
if (cw.notModified === true) { if (cw.notModified === true) {
return next(); return next();
} }
const removeDirs = (dir) => { const removeDirs = (dir: DirectoryDTO) => {
dir.photos.forEach((photo: PhotoDTO) => { dir.media.forEach((photo: PhotoDTO) => {
photo.directory = null; photo.directory = null;
}); });
@ -119,28 +119,28 @@ export class GalleryMWs {
return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'No photo found')); return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'No photo found'));
} }
req.params.imagePath = path.join(photo.directory.path, photo.directory.name, photo.name); req.params.mediaPath = path.join(photo.directory.path, photo.directory.name, photo.name);
return next(); return next();
} }
public static loadImage(req: Request, res: Response, next: NextFunction) { public static loadMedia(req: Request, res: Response, next: NextFunction) {
if (!(req.params.imagePath)) { if (!(req.params.mediaPath)) {
return next(); return next();
} }
const fullImagePath = path.join(ProjectPath.ImageFolder, req.params.imagePath); const fullMediaPath = path.join(ProjectPath.ImageFolder, req.params.mediaPath);
// check if thumbnail already exist // check if thumbnail already exist
if (fs.existsSync(fullImagePath) === false) { if (fs.existsSync(fullMediaPath) === false) {
return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'no such file:' + fullImagePath)); return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'no such file:' + fullMediaPath));
} }
if (fs.statSync(fullImagePath).isDirectory()) { if (fs.statSync(fullMediaPath).isDirectory()) {
return next(); return next();
} }
req.resultPipe = fullImagePath; req.resultPipe = fullMediaPath;
return next(); return next();
} }
@ -161,7 +161,7 @@ export class GalleryMWs {
try { try {
const result = await ObjectManagerRepository.getInstance().SearchManager.search(req.params.text, type); const result = await ObjectManagerRepository.getInstance().SearchManager.search(req.params.text, type);
result.directories.forEach(dir => dir.photos = dir.photos || []); result.directories.forEach(dir => dir.media = dir.media || []);
req.resultPipe = new ContentWrapper(null, result); req.resultPipe = new ContentWrapper(null, result);
return next(); return next();
} catch (err) { } catch (err) {
@ -181,7 +181,7 @@ export class GalleryMWs {
try { try {
const result = await ObjectManagerRepository.getInstance().SearchManager.instantSearch(req.params.text); const result = await ObjectManagerRepository.getInstance().SearchManager.instantSearch(req.params.text);
result.directories.forEach(dir => dir.photos = dir.photos || []); result.directories.forEach(dir => dir.media = dir.media || []);
req.resultPipe = new ContentWrapper(null, result); req.resultPipe = new ContentWrapper(null, result);
return next(); return next();
} catch (err) { } catch (err) {

View File

@ -12,8 +12,9 @@ import {PhotoDTO} from '../../../common/entities/PhotoDTO';
import {Config} from '../../../common/config/private/Config'; import {Config} from '../../../common/config/private/Config';
import {ThumbnailProcessingLib} from '../../../common/config/private/IPrivateConfig'; import {ThumbnailProcessingLib} from '../../../common/config/private/IPrivateConfig';
import {ThumbnailTH} from '../../model/threading/ThreadPool'; import {ThumbnailTH} from '../../model/threading/ThreadPool';
import {RendererInput} from '../../model/threading/ThumbnailWorker'; import {RendererInput, ThumbnailSourceType} from '../../model/threading/ThumbnailWorker';
import {ITaskQue, TaskQue} from '../../model/threading/TaskQue'; import {ITaskQue, TaskQue} from '../../model/threading/TaskQue';
import {MediaDTO} from '../../../common/entities/MediaDTO';
export class ThumbnailGeneratorMWs { export class ThumbnailGeneratorMWs {
@ -60,7 +61,7 @@ export class ThumbnailGeneratorMWs {
ThumbnailGeneratorMWs.addThInfoTODir(<DirectoryDTO>cw.directory); ThumbnailGeneratorMWs.addThInfoTODir(<DirectoryDTO>cw.directory);
} }
if (cw.searchResult) { if (cw.searchResult) {
ThumbnailGeneratorMWs.addThInfoToPhotos(cw.searchResult.photos); ThumbnailGeneratorMWs.addThInfoToPhotos(cw.searchResult.media);
} }
@ -68,13 +69,14 @@ export class ThumbnailGeneratorMWs {
} }
public static generateThumbnail(req: Request, res: Response, next: NextFunction) { public static generateThumbnailFactory(sourceType: ThumbnailSourceType) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.resultPipe) { if (!req.resultPipe) {
return next(); return next();
} }
// load parameters // load parameters
const imagePath = req.resultPipe; const mediaPath = req.resultPipe;
let size: number = parseInt(req.params.size, 10) || Config.Client.Thumbnail.thumbnailSizes[0]; let size: number = parseInt(req.params.size, 10) || Config.Client.Thumbnail.thumbnailSizes[0];
// validate size // validate size
@ -82,32 +84,32 @@ export class ThumbnailGeneratorMWs {
size = Config.Client.Thumbnail.thumbnailSizes[0]; size = Config.Client.Thumbnail.thumbnailSizes[0];
} }
ThumbnailGeneratorMWs.generateImage(imagePath, size, false, req, res, next); ThumbnailGeneratorMWs.generateImage(mediaPath, size, sourceType, false, req, res, next);
};
} }
public static generateIcon(req: Request, res: Response, next: NextFunction) { public static generateIconFactory(sourceType: ThumbnailSourceType) {
return (req: Request, res: Response, next: NextFunction) => {
if (!req.resultPipe) { if (!req.resultPipe) {
return next(); return next();
} }
// load parameters // load parameters
const imagePath = req.resultPipe; const mediaPath = req.resultPipe;
const size: number = Config.Client.Thumbnail.iconSize; const size: number = Config.Client.Thumbnail.iconSize;
ThumbnailGeneratorMWs.generateImage(imagePath, size, true, req, res, next); ThumbnailGeneratorMWs.generateImage(mediaPath, size, sourceType, true, req, res, next);
};
} }
private static addThInfoTODir(directory: DirectoryDTO) { private static addThInfoTODir(directory: DirectoryDTO) {
if (typeof directory.photos === 'undefined') { if (typeof directory.media === 'undefined') {
directory.photos = []; directory.media = [];
} }
if (typeof directory.directories === 'undefined') { if (typeof directory.directories === 'undefined') {
directory.directories = []; directory.directories = [];
} }
ThumbnailGeneratorMWs.addThInfoToPhotos(directory.photos); ThumbnailGeneratorMWs.addThInfoToPhotos(directory.media);
for (let i = 0; i < directory.directories.length; i++) { for (let i = 0; i < directory.directories.length; i++) {
ThumbnailGeneratorMWs.addThInfoTODir(directory.directories[i]); ThumbnailGeneratorMWs.addThInfoTODir(directory.directories[i]);
@ -115,13 +117,13 @@ export class ThumbnailGeneratorMWs {
} }
private static addThInfoToPhotos(photos: Array<PhotoDTO>) { private static addThInfoToPhotos(photos: MediaDTO[]) {
const thumbnailFolder = ProjectPath.ThumbnailFolder; const thumbnailFolder = ProjectPath.ThumbnailFolder;
for (let i = 0; i < photos.length; i++) { for (let i = 0; i < photos.length; i++) {
const fullImagePath = path.join(ProjectPath.ImageFolder, photos[i].directory.path, photos[i].directory.name, photos[i].name); const fullMediaPath = path.join(ProjectPath.ImageFolder, photos[i].directory.path, photos[i].directory.name, photos[i].name);
for (let j = 0; j < Config.Client.Thumbnail.thumbnailSizes.length; j++) { for (let j = 0; j < Config.Client.Thumbnail.thumbnailSizes.length; j++) {
const size = Config.Client.Thumbnail.thumbnailSizes[j]; const size = Config.Client.Thumbnail.thumbnailSizes[j];
const thPath = path.join(thumbnailFolder, ThumbnailGeneratorMWs.generateThumbnailName(fullImagePath, size)); const thPath = path.join(thumbnailFolder, ThumbnailGeneratorMWs.generateThumbnailName(fullMediaPath, size));
if (fs.existsSync(thPath) === true) { if (fs.existsSync(thPath) === true) {
if (typeof photos[i].readyThumbnails === 'undefined') { if (typeof photos[i].readyThumbnails === 'undefined') {
photos[i].readyThumbnails = []; photos[i].readyThumbnails = [];
@ -130,7 +132,7 @@ export class ThumbnailGeneratorMWs {
} }
} }
const iconPath = path.join(thumbnailFolder, const iconPath = path.join(thumbnailFolder,
ThumbnailGeneratorMWs.generateThumbnailName(fullImagePath, Config.Client.Thumbnail.iconSize)); ThumbnailGeneratorMWs.generateThumbnailName(fullMediaPath, Config.Client.Thumbnail.iconSize));
if (fs.existsSync(iconPath) === true) { if (fs.existsSync(iconPath) === true) {
photos[i].readyIcon = true; photos[i].readyIcon = true;
} }
@ -138,12 +140,13 @@ export class ThumbnailGeneratorMWs {
} }
} }
private static async generateImage(imagePath: string, private static async generateImage(mediaPath: string,
size: number, size: number,
sourceType: ThumbnailSourceType,
makeSquare: boolean, makeSquare: boolean,
req: Request, res: Response, next: NextFunction) { req: Request, res: Response, next: NextFunction) {
// generate thumbnail path // generate thumbnail path
const thPath = path.join(ProjectPath.ThumbnailFolder, ThumbnailGeneratorMWs.generateThumbnailName(imagePath, size)); const thPath = path.join(ProjectPath.ThumbnailFolder, ThumbnailGeneratorMWs.generateThumbnailName(mediaPath, size));
req.resultPipe = thPath; req.resultPipe = thPath;
@ -160,7 +163,8 @@ export class ThumbnailGeneratorMWs {
// run on other thread // run on other thread
const input = <RendererInput>{ const input = <RendererInput>{
imagePath: imagePath, type: sourceType,
mediaPath: mediaPath,
size: size, size: size,
thPath: thPath, thPath: thPath,
makeSquare: makeSquare, makeSquare: makeSquare,
@ -170,12 +174,12 @@ export class ThumbnailGeneratorMWs {
await this.taskQue.execute(input); await this.taskQue.execute(input);
return next(); return next();
} catch (error) { } catch (error) {
return next(new ErrorDTO(ErrorCodes.THUMBNAIL_GENERATION_ERROR, 'Error during generating thumbnail: ' + input.imagePath, error)); return next(new ErrorDTO(ErrorCodes.THUMBNAIL_GENERATION_ERROR, 'Error during generating thumbnail: ' + input.mediaPath, error));
} }
} }
private static generateThumbnailName(imagePath: string, size: number): string { private static generateThumbnailName(mediaPath: string, size: number): string {
return crypto.createHash('md5').update(imagePath).digest('hex') + '_' + size + '.jpg'; return crypto.createHash('md5').update(mediaPath).digest('hex') + '_' + size + '.jpg';
} }
} }

View File

@ -204,9 +204,9 @@ export class ConfigDiagnostics {
await ConfigDiagnostics.testRandomPhotoConfig(Config.Client.Sharing, Config); await ConfigDiagnostics.testRandomPhotoConfig(Config.Client.Sharing, Config);
} catch (ex) { } catch (ex) {
const err: Error = ex; const err: Error = ex;
NotificationManager.warning('Random Photo is not supported with these settings. Disabling temporally. ' + NotificationManager.warning('Random Media is not supported with these settings. Disabling temporally. ' +
'Please adjust the config properly.', err.toString()); 'Please adjust the config properly.', err.toString());
Logger.warn(LOG_TAG, 'Random Photo is not supported with these settings, switching off..', err.toString()); Logger.warn(LOG_TAG, 'Random Media is not supported with these settings, switching off..', err.toString());
Config.Client.Sharing.enabled = false; Config.Client.Sharing.enabled = false;
} }

View File

@ -28,7 +28,7 @@ export class DiskManager {
directory = await DiskMangerWorker.scanDirectory(relativeDirectoryName); directory = await DiskMangerWorker.scanDirectory(relativeDirectoryName);
} }
const addDirs = (dir: DirectoryDTO) => { const addDirs = (dir: DirectoryDTO) => {
dir.photos.forEach((ph) => { dir.media.forEach((ph) => {
ph.directory = dir; ph.directory = dir;
}); });
dir.directories.forEach((d) => { dir.directories.forEach((d) => {

View File

@ -25,6 +25,6 @@ export class GalleryManager implements IGalleryManager {
} }
getRandomPhoto(RandomQuery): Promise<PhotoDTO> { getRandomPhoto(RandomQuery): Promise<PhotoDTO> {
throw new Error('Random photo is not supported without database'); throw new Error('Random media is not supported without database');
} }
} }

View File

@ -53,30 +53,30 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
return null; return null;
} }
} }
if (dir.photos) { if (dir.media) {
for (let i = 0; i < dir.photos.length; i++) { for (let i = 0; i < dir.media.length; i++) {
dir.photos[i].directory = dir; dir.media[i].directory = dir;
dir.photos[i].readyThumbnails = []; dir.media[i].readyThumbnails = [];
dir.photos[i].readyIcon = false; dir.media[i].readyIcon = false;
} }
} }
if (dir.directories) { if (dir.directories) {
for (let i = 0; i < dir.directories.length; i++) { for (let i = 0; i < dir.directories.length; i++) {
dir.directories[i].photos = await connection dir.directories[i].media = await connection
.getRepository(PhotoEntity) .getRepository(PhotoEntity)
.createQueryBuilder('photo') .createQueryBuilder('media')
.where('photo.directory = :dir', { .where('media.directory = :dir', {
dir: dir.directories[i].id dir: dir.directories[i].id
}) })
.orderBy('photo.metadata.creationDate', 'ASC') .orderBy('media.metadata.creationDate', 'ASC')
.limit(Config.Server.indexing.folderPreviewSize) .limit(Config.Server.indexing.folderPreviewSize)
.getMany(); .getMany();
dir.directories[i].isPartial = true; dir.directories[i].isPartial = true;
for (let j = 0; j < dir.directories[i].photos.length; j++) { for (let j = 0; j < dir.directories[i].media.length; j++) {
dir.directories[i].photos[j].directory = dir.directories[i]; dir.directories[i].media[j].directory = dir.directories[i];
dir.directories[i].photos[j].readyThumbnails = []; dir.directories[i].media[j].readyThumbnails = [];
dir.directories[i].photos[j].readyIcon = false; dir.directories[i].media[j].readyIcon = false;
} }
} }
} }
@ -113,7 +113,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
const scannedDirectory = await DiskManager.scanDirectory(relativeDirectoryName); const scannedDirectory = await DiskManager.scanDirectory(relativeDirectoryName);
// returning with the result // returning with the result
scannedDirectory.photos.forEach(p => p.readyThumbnails = []); scannedDirectory.media.forEach(p => p.readyThumbnails = []);
resolve(scannedDirectory); resolve(scannedDirectory);
await this.saveToDB(scannedDirectory); await this.saveToDB(scannedDirectory);
@ -169,18 +169,18 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
if (directory != null) { // update existing directory if (directory != null) { // update existing directory
if (!directory.parent || !directory.parent.id) { // set parent if not set yet if (!directory.parent || !directory.parent.id) { // set parent if not set yet
directory.parent = currentDir; directory.parent = currentDir;
delete directory.photos; delete directory.media;
await directoryRepository.save(directory); await directoryRepository.save(directory);
} }
} else { } else {
scannedDirectory.directories[i].parent = currentDir; scannedDirectory.directories[i].parent = currentDir;
(<DirectoryEntity>scannedDirectory.directories[i]).lastScanned = null; // new child dir, not fully scanned yet (<DirectoryEntity>scannedDirectory.directories[i]).lastScanned = null; // new child dir, not fully scanned yet
const d = await directoryRepository.save(<DirectoryEntity>scannedDirectory.directories[i]); const d = await directoryRepository.save(<DirectoryEntity>scannedDirectory.directories[i]);
for (let j = 0; j < scannedDirectory.directories[i].photos.length; j++) { for (let j = 0; j < scannedDirectory.directories[i].media.length; j++) {
scannedDirectory.directories[i].photos[j].directory = d; scannedDirectory.directories[i].media[j].directory = d;
} }
await photosRepository.save(scannedDirectory.directories[i].photos); await photosRepository.save(scannedDirectory.directories[i].media);
} }
} }
@ -188,38 +188,38 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
await directoryRepository.remove(childDirectories); await directoryRepository.remove(childDirectories);
const indexedPhotos = await photosRepository.createQueryBuilder('photo') const indexedPhotos = await photosRepository.createQueryBuilder('media')
.where('photo.directory = :dir', { .where('media.directory = :dir', {
dir: currentDir.id dir: currentDir.id
}).getMany(); }).getMany();
const photosToSave = []; const photosToSave = [];
for (let i = 0; i < scannedDirectory.photos.length; i++) { for (let i = 0; i < scannedDirectory.media.length; i++) {
let photo = null; let photo = null;
for (let j = 0; j < indexedPhotos.length; j++) { for (let j = 0; j < indexedPhotos.length; j++) {
if (indexedPhotos[j].name === scannedDirectory.photos[i].name) { if (indexedPhotos[j].name === scannedDirectory.media[i].name) {
photo = indexedPhotos[j]; photo = indexedPhotos[j];
indexedPhotos.splice(j, 1); indexedPhotos.splice(j, 1);
break; break;
} }
} }
if (photo == null) { if (photo == null) {
scannedDirectory.photos[i].directory = null; scannedDirectory.media[i].directory = null;
photo = Utils.clone(scannedDirectory.photos[i]); photo = Utils.clone(scannedDirectory.media[i]);
scannedDirectory.photos[i].directory = scannedDirectory; scannedDirectory.media[i].directory = scannedDirectory;
photo.directory = currentDir; photo.directory = currentDir;
} }
if (photo.metadata.keywords !== scannedDirectory.photos[i].metadata.keywords || if (photo.metadata.keywords !== scannedDirectory.media[i].metadata.keywords ||
photo.metadata.cameraData !== scannedDirectory.photos[i].metadata.cameraData || photo.metadata.cameraData !== (<PhotoDTO>scannedDirectory.media[i]).metadata.cameraData ||
photo.metadata.positionData !== scannedDirectory.photos[i].metadata.positionData || photo.metadata.positionData !== scannedDirectory.media[i].metadata.positionData ||
photo.metadata.size !== scannedDirectory.photos[i].metadata.size) { photo.metadata.size !== scannedDirectory.media[i].metadata.size) {
photo.metadata.keywords = scannedDirectory.photos[i].metadata.keywords; photo.metadata.keywords = scannedDirectory.media[i].metadata.keywords;
photo.metadata.cameraData = scannedDirectory.photos[i].metadata.cameraData; photo.metadata.cameraData = (<PhotoDTO>scannedDirectory.media[i]).metadata.cameraData;
photo.metadata.positionData = scannedDirectory.photos[i].metadata.positionData; photo.metadata.positionData = scannedDirectory.media[i].metadata.positionData;
photo.metadata.size = scannedDirectory.photos[i].metadata.size; photo.metadata.size = scannedDirectory.media[i].metadata.size;
photosToSave.push(photo); photosToSave.push(photo);
} }
} }
@ -233,8 +233,8 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
const connection = await SQLConnection.getConnection(); const connection = await SQLConnection.getConnection();
const photosRepository = connection.getRepository(PhotoEntity); const photosRepository = connection.getRepository(PhotoEntity);
const query = photosRepository.createQueryBuilder('photo'); const query = photosRepository.createQueryBuilder('media');
query.innerJoinAndSelect('photo.directory', 'directory'); query.innerJoinAndSelect('media.directory', 'directory');
if (queryFilter.directory) { if (queryFilter.directory) {
const directoryName = path.basename(queryFilter.directory); const directoryName = path.basename(queryFilter.directory);
@ -253,31 +253,31 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager {
} }
if (queryFilter.fromDate) { if (queryFilter.fromDate) {
query.andWhere('photo.metadata.creationDate >= :fromDate', { query.andWhere('media.metadata.creationDate >= :fromDate', {
fromDate: queryFilter.fromDate.getTime() fromDate: queryFilter.fromDate.getTime()
}); });
} }
if (queryFilter.toDate) { if (queryFilter.toDate) {
query.andWhere('photo.metadata.creationDate <= :toDate', { query.andWhere('media.metadata.creationDate <= :toDate', {
toDate: queryFilter.toDate.getTime() toDate: queryFilter.toDate.getTime()
}); });
} }
if (queryFilter.minResolution) { if (queryFilter.minResolution) {
query.andWhere('photo.metadata.size.width * photo.metadata.size.height >= :minRes', { query.andWhere('media.metadata.size.width * media.metadata.size.height >= :minRes', {
minRes: queryFilter.minResolution * 1000 * 1000 minRes: queryFilter.minResolution * 1000 * 1000
}); });
} }
if (queryFilter.maxResolution) { if (queryFilter.maxResolution) {
query.andWhere('photo.metadata.size.width * photo.metadata.size.height <= :maxRes', { query.andWhere('media.metadata.size.width * media.metadata.size.height <= :maxRes', {
maxRes: queryFilter.maxResolution * 1000 * 1000 maxRes: queryFilter.maxResolution * 1000 * 1000
}); });
} }
if (queryFilter.orientation === OrientationType.landscape) { if (queryFilter.orientation === OrientationType.landscape) {
query.andWhere('photo.metadata.size.width >= photo.metadata.size.height'); query.andWhere('media.metadata.size.width >= media.metadata.size.height');
} }
if (queryFilter.orientation === OrientationType.portrait) { if (queryFilter.orientation === OrientationType.portrait) {
query.andWhere('photo.metadata.size.width <= photo.metadata.size.height'); query.andWhere('media.metadata.size.width <= media.metadata.size.height');
} }

View File

@ -30,9 +30,9 @@ export class SearchManager implements ISearchManager {
(await photoRepository (await photoRepository
.createQueryBuilder('photo') .createQueryBuilder('media')
.select('DISTINCT(photo.metadata.keywords)') .select('DISTINCT(media.metadata.keywords)')
.where('photo.metadata.keywords LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) .where('media.metadata.keywords LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'})
.limit(5) .limit(5)
.getRawMany()) .getRawMany())
.map(r => <Array<string>>r.metadataKeywords.split(',')) .map(r => <Array<string>>r.metadataKeywords.split(','))
@ -43,13 +43,13 @@ export class SearchManager implements ISearchManager {
(await photoRepository (await photoRepository
.createQueryBuilder('photo') .createQueryBuilder('media')
.select('photo.metadata.positionData.country as country,' + .select('media.metadata.positionData.country as country,' +
' photo.metadata.positionData.state as state, photo.metadata.positionData.city as city') 'mediao.metadata.positionData.state as state, media.metadata.positionData.city as city')
.where('photo.metadata.positionData.country LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) .where('media.metadata.positionData.country LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'})
.orWhere('photo.metadata.positionData.state LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) .orWhere('media.metadata.positionData.state LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'})
.orWhere('photo.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) .orWhere('media.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'})
.groupBy('photo.metadata.positionData.country, photo.metadata.positionData.state, photo.metadata.positionData.city') .groupBy('media.metadata.positionData.country, media.metadata.positionData.state, media.metadata.positionData.city')
.limit(5) .limit(5)
.getRawMany()) .getRawMany())
.filter(pm => !!pm) .filter(pm => !!pm)
@ -60,9 +60,9 @@ export class SearchManager implements ISearchManager {
}); });
result = result.concat(this.encapsulateAutoComplete((await photoRepository result = result.concat(this.encapsulateAutoComplete((await photoRepository
.createQueryBuilder('photo') .createQueryBuilder('media')
.select('DISTINCT(photo.name)') .select('DISTINCT(media.name)')
.where('photo.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) .where('media.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'})
.limit(5) .limit(5)
.getRawMany()) .getRawMany())
.map(r => r.name), SearchTypes.image)); .map(r => r.name), SearchTypes.image));
@ -86,15 +86,15 @@ export class SearchManager implements ISearchManager {
searchText: text, searchText: text,
searchType: searchType, searchType: searchType,
directories: [], directories: [],
photos: [], media: [],
resultOverflow: false resultOverflow: false
}; };
const query = connection const query = connection
.getRepository(PhotoEntity) .getRepository(PhotoEntity)
.createQueryBuilder('photo') .createQueryBuilder('media')
.innerJoinAndSelect('photo.directory', 'directory') .innerJoinAndSelect('media.directory', 'directory')
.orderBy('photo.metadata.creationDate', 'ASC'); .orderBy('media.metadata.creationDate', 'ASC');
if (!searchType || searchType === SearchTypes.directory) { if (!searchType || searchType === SearchTypes.directory) {
@ -102,24 +102,24 @@ export class SearchManager implements ISearchManager {
} }
if (!searchType || searchType === SearchTypes.image) { if (!searchType || searchType === SearchTypes.image) {
query.orWhere('photo.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); query.orWhere('media.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'});
} }
if (!searchType || searchType === SearchTypes.position) { if (!searchType || searchType === SearchTypes.position) {
query.orWhere('photo.metadata.positionData.country LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) query.orWhere('media.metadata.positionData.country LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'})
.orWhere('photo.metadata.positionData.state LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) .orWhere('media.metadata.positionData.state LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'})
.orWhere('photo.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); .orWhere('media.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'});
} }
if (!searchType || searchType === SearchTypes.keyword) { if (!searchType || searchType === SearchTypes.keyword) {
query.orWhere('photo.metadata.keywords LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); query.orWhere('media.metadata.keywords LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'});
} }
result.photos = await query result.media = await query
.limit(2001) .limit(2001)
.getMany(); .getMany();
if (result.photos.length > 2000) { if (result.media.length > 2000) {
result.resultOverflow = true; result.resultOverflow = true;
} }
@ -144,20 +144,20 @@ export class SearchManager implements ISearchManager {
searchText: text, searchText: text,
// searchType:undefined, not adding this // searchType:undefined, not adding this
directories: [], directories: [],
photos: [], media: [],
resultOverflow: false resultOverflow: false
}; };
result.photos = await connection result.media = await connection
.getRepository(PhotoEntity) .getRepository(PhotoEntity)
.createQueryBuilder('photo') .createQueryBuilder('media')
.orderBy('photo.metadata.creationDate', 'ASC') .orderBy('media.metadata.creationDate', 'ASC')
.where('photo.metadata.keywords LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) .where('media.metadata.keywords LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'})
.orWhere('photo.metadata.positionData.country LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) .orWhere('media.metadata.positionData.country LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'})
.orWhere('photo.metadata.positionData.state LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) .orWhere('media.metadata.positionData.state LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'})
.orWhere('photo.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) .orWhere('media.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'})
.orWhere('photo.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) .orWhere('media.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'})
.innerJoinAndSelect('photo.directory', 'directory') .innerJoinAndSelect('media.directory', 'directory')
.limit(10) .limit(10)
.getMany(); .getMany();

View File

@ -15,13 +15,13 @@ export class DirectoryEntity implements DirectoryDTO {
path: string; path: string;
/** /**
* last time the directory was modified (from outside, eg.: a new photo was added) * last time the directory was modified (from outside, eg.: a new media was added)
*/ */
@Column('bigint') @Column('bigint')
public lastModified: number; public lastModified: number;
/** /**
* Last time the directory was fully scanned, not only for a few photos to create a preview * Last time the directory was fully scanned, not only for a few media to create a preview
*/ */
@Column({type: 'bigint', nullable: true}) @Column({type: 'bigint', nullable: true})
public lastScanned: number; public lastScanned: number;
@ -35,6 +35,6 @@ export class DirectoryEntity implements DirectoryDTO {
public directories: Array<DirectoryEntity>; public directories: Array<DirectoryEntity>;
@OneToMany(type => PhotoEntity, photo => photo.directory) @OneToMany(type => PhotoEntity, photo => photo.directory)
public photos: Array<PhotoEntity>; public media: Array<PhotoEntity>;
} }

View File

@ -1,7 +1,8 @@
import {Column, Entity, ManyToOne, PrimaryGeneratedColumn} from 'typeorm'; import {Column, Entity, ManyToOne, PrimaryGeneratedColumn} from 'typeorm';
import {CameraMetadata, GPSMetadata, ImageSize, PhotoDTO, PhotoMetadata, PositionMetaData} from '../../../../common/entities/PhotoDTO'; import {CameraMetadata, PhotoDTO, PhotoMetadata} from '../../../../common/entities/PhotoDTO';
import {DirectoryEntity} from './DirectoryEntity'; import {DirectoryEntity} from './DirectoryEntity';
import {OrientationTypes} from 'ts-exif-parser'; import {OrientationTypes} from 'ts-exif-parser';
import {GPSMetadata, MediaDimension, PositionMetaData} from '../../../../common/entities/MediaDTO';
@Entity() @Entity()
export class CameraMetadataEntity implements CameraMetadata { export class CameraMetadataEntity implements CameraMetadata {
@ -41,7 +42,7 @@ export class GPSMetadataEntity implements GPSMetadata {
} }
@Entity() @Entity()
export class ImageSizeEntity implements ImageSize { export class ImageSizeEntity implements MediaDimension {
@Column('int') @Column('int')
width: number; width: number;
@ -103,7 +104,7 @@ export class PhotoEntity implements PhotoDTO {
@Column('text') @Column('text')
name: string; name: string;
@ManyToOne(type => DirectoryEntity, directory => directory.photos, {onDelete: 'CASCADE'}) @ManyToOne(type => DirectoryEntity, directory => directory.media, {onDelete: 'CASCADE'})
directory: DirectoryEntity; directory: DirectoryEntity;
@Column(type => PhotoMetadataEntity) @Column(type => PhotoMetadataEntity)

View File

@ -1,12 +1,16 @@
import * as fs from 'fs'; import * as fs from 'fs';
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} from '../../../common/entities/PhotoDTO'; import {CameraMetadata, PhotoDTO, PhotoMetadata} from '../../../common/entities/PhotoDTO';
import {Logger} from '../../Logger'; import {Logger} from '../../Logger';
import {IptcParser} from 'ts-node-iptc'; import {IptcParser} from 'ts-node-iptc';
import {ExifParserFactory, OrientationTypes} from 'ts-exif-parser'; import {ExifParserFactory, OrientationTypes} from 'ts-exif-parser';
import * as ffmpeg from 'fluent-ffmpeg';
import {FfmpegCommand, FfprobeData} from 'fluent-ffmpeg';
import {ProjectPath} from '../../ProjectPath'; import {ProjectPath} from '../../ProjectPath';
import {Config} from '../../../common/config/private/Config'; import {Config} from '../../../common/config/private/Config';
import {VideoDTO} from '../../../common/entities/VideoDTO';
import {GPSMetadata, MediaDimension, MediaMetadata} from '../../../common/entities/MediaDTO';
const LOG_TAG = '[DiskManagerTask]'; const LOG_TAG = '[DiskManagerTask]';
@ -27,6 +31,16 @@ export class DiskMangerWorker {
return extensions.indexOf(extension) !== -1; return extensions.indexOf(extension) !== -1;
} }
private static isVideo(fullPath: string) {
const extensions = [
'.mp4',
'.webm'
];
const extension = path.extname(fullPath).toLowerCase();
return extensions.indexOf(extension) !== -1;
}
public static scanDirectory(relativeDirectoryName: string, maxPhotos: number = null, photosOnly: boolean = false): Promise<DirectoryDTO> { public static scanDirectory(relativeDirectoryName: string, maxPhotos: number = null, photosOnly: boolean = false): Promise<DirectoryDTO> {
return new Promise<DirectoryDTO>((resolve, reject) => { return new Promise<DirectoryDTO>((resolve, reject) => {
const directoryName = path.basename(relativeDirectoryName); const directoryName = path.basename(relativeDirectoryName);
@ -41,9 +55,9 @@ export class DiskMangerWorker {
lastScanned: Date.now(), lastScanned: Date.now(),
directories: [], directories: [],
isPartial: false, isPartial: false,
photos: [] media: []
}; };
fs.readdir(absoluteDirectoryName, async (err, list) => { fs.readdir(absoluteDirectoryName, async (err, list: string[]) => {
if (err) { if (err) {
return reject(err); return reject(err);
} }
@ -60,13 +74,23 @@ export class DiskMangerWorker {
d.isPartial = true; d.isPartial = true;
directory.directories.push(d); directory.directories.push(d);
} else if (DiskMangerWorker.isImage(fullFilePath)) { } else if (DiskMangerWorker.isImage(fullFilePath)) {
directory.photos.push(<PhotoDTO>{ directory.media.push(<PhotoDTO>{
name: file, name: file,
directory: null, directory: null,
metadata: await DiskMangerWorker.loadPhotoMetadata(fullFilePath) metadata: await DiskMangerWorker.loadPhotoMetadata(fullFilePath)
}); });
if (maxPhotos != null && directory.photos.length > maxPhotos) { if (maxPhotos != null && directory.media.length > maxPhotos) {
break;
}
} else if (DiskMangerWorker.isVideo(fullFilePath)) {
directory.media.push(<VideoDTO>{
name: file,
directory: null,
metadata: await DiskMangerWorker.loadPVideoMetadata(fullFilePath)
});
if (maxPhotos != null && directory.media.length > maxPhotos) {
break; break;
} }
} }
@ -82,6 +106,44 @@ export class DiskMangerWorker {
} }
private static loadPVideoMetadata(fullPath: string): Promise<MediaMetadata> {
return new Promise<MediaMetadata>((resolve, reject) => {
const metadata: MediaMetadata = <MediaMetadata>{
keywords: [],
positionData: null,
size: {
width: 0,
height: 0
},
orientation: OrientationTypes.TOP_LEFT,
creationDate: 0,
fileSize: 0
};
try {
const stat = fs.statSync(fullPath);
metadata.fileSize = stat.size;
} catch (err) {
}
ffmpeg(fullPath).ffprobe((err: any, data: FfprobeData) => {
if (!!err || data === null) {
return reject(err);
}
metadata.size = {
width: data.streams[0].width,
height: data.streams[0].height
};
try {
metadata.creationDate = data.streams[0].tags.creation_time;
} catch (err) {
}
return resolve(metadata);
});
});
}
private static loadPhotoMetadata(fullPath: string): Promise<PhotoMetadata> { private static loadPhotoMetadata(fullPath: string): Promise<PhotoMetadata> {
return new Promise<PhotoMetadata>((resolve, reject) => { return new Promise<PhotoMetadata>((resolve, reject) => {
fs.readFile(fullPath, (err, data) => { fs.readFile(fullPath, (err, data) => {
@ -135,15 +197,15 @@ export class DiskMangerWorker {
if (exif.imageSize) { if (exif.imageSize) {
metadata.size = <ImageSize> {width: exif.imageSize.width, height: exif.imageSize.height}; metadata.size = <MediaDimension> {width: exif.imageSize.width, height: exif.imageSize.height};
} else if (exif.tags.RelatedImageWidth && exif.tags.RelatedImageHeight) { } else if (exif.tags.RelatedImageWidth && exif.tags.RelatedImageHeight) {
metadata.size = <ImageSize> {width: exif.tags.RelatedImageWidth, height: exif.tags.RelatedImageHeight}; metadata.size = <MediaDimension> {width: exif.tags.RelatedImageWidth, height: exif.tags.RelatedImageHeight};
} else { } else {
metadata.size = <ImageSize> {width: 1, height: 1}; metadata.size = <MediaDimension> {width: 1, height: 1};
} }
} catch (err) { } catch (err) {
Logger.debug(LOG_TAG, 'Error parsing exif', fullPath, err); Logger.debug(LOG_TAG, 'Error parsing exif', fullPath, err);
metadata.size = <ImageSize> {width: 1, height: 1}; metadata.size = <MediaDimension> {width: 1, height: 1};
} }
try { try {

View File

@ -1,43 +1,103 @@
import {Metadata, Sharp} from 'sharp'; import {Metadata, Sharp} from 'sharp';
import {Dimensions, State} from 'gm'; import {Dimensions, State} from 'gm';
import {Logger} from '../../Logger'; import {Logger} from '../../Logger';
import {FfmpegCommand, FfprobeData} from 'fluent-ffmpeg';
import {ThumbnailProcessingLib} from '../../../common/config/private/IPrivateConfig'; import {ThumbnailProcessingLib} from '../../../common/config/private/IPrivateConfig';
export class ThumbnailWorker { export class ThumbnailWorker {
private static renderer: (input: RendererInput) => Promise<void> = null; private static imageRenderer: (input: RendererInput) => Promise<void> = null;
private static videoRenderer: (input: RendererInput) => Promise<void> = null;
private static rendererType = null; private static rendererType = null;
public static render(input: RendererInput, renderer: ThumbnailProcessingLib): Promise<void> { public static render(input: RendererInput, renderer: ThumbnailProcessingLib): Promise<void> {
if (input.type === ThumbnailSourceType.Image) {
return this.renderFromImage(input, renderer);
}
return this.renderFromVideo(input);
}
public static renderFromImage(input: RendererInput, renderer: ThumbnailProcessingLib): Promise<void> {
if (ThumbnailWorker.rendererType !== renderer) { if (ThumbnailWorker.rendererType !== renderer) {
ThumbnailWorker.renderer = RendererFactory.build(renderer); ThumbnailWorker.imageRenderer = ImageRendererFactory.build(renderer);
ThumbnailWorker.rendererType = renderer; ThumbnailWorker.rendererType = renderer;
} }
return ThumbnailWorker.renderer(input); return ThumbnailWorker.imageRenderer(input);
} }
public static renderFromVideo(input: RendererInput): Promise<void> {
if (ThumbnailWorker.videoRenderer === null) {
ThumbnailWorker.videoRenderer = VideoRendererFactory.build();
}
return ThumbnailWorker.videoRenderer(input);
} }
}
export enum ThumbnailSourceType {
Image, Video
}
export interface RendererInput { export interface RendererInput {
imagePath: string; type: ThumbnailSourceType;
mediaPath: string;
size: number; size: number;
makeSquare: boolean; makeSquare: boolean;
thPath: string; thPath: string;
qualityPriority: boolean; qualityPriority: boolean;
} }
export class RendererFactory { export class VideoRendererFactory {
public static build(): (input: RendererInput) => Promise<void> {
const ffmpeg = require('fluent-ffmpeg');
return (input: RendererInput): Promise<void> => {
return new Promise((resolve, reject) => {
Logger.silly('[FFmpeg] rendering thumbnail: ' + input.mediaPath);
ffmpeg(input.mediaPath).ffprobe((err: any, data: FfprobeData) => {
if (!!err || data === null) {
return reject(err);
}
const ratio = data.streams[0].height / data.streams[0].width;
const command: FfmpegCommand = ffmpeg(input.mediaPath);
command
.on('end', () => {
resolve();
})
.on('error', (e) => {
reject(e);
})
.outputOptions(['-qscale:v 4']);
if (input.makeSquare === false) {
const newWidth = Math.round(Math.sqrt((input.size * input.size) / ratio));
command.takeScreenshots({
timemarks: ['10%'], size: newWidth + 'x?', filename: input.thPath
});
} else {
command.takeScreenshots({
timemarks: ['10%'], size: input.size + 'x' + input.size, filename: input.thPath
});
}
});
});
};
}
}
export class ImageRendererFactory {
public static build(renderer: ThumbnailProcessingLib): (input: RendererInput) => Promise<void> { public static build(renderer: ThumbnailProcessingLib): (input: RendererInput) => Promise<void> {
switch (renderer) { switch (renderer) {
case ThumbnailProcessingLib.Jimp: case ThumbnailProcessingLib.Jimp:
return RendererFactory.Jimp(); return ImageRendererFactory.Jimp();
case ThumbnailProcessingLib.gm: case ThumbnailProcessingLib.gm:
return RendererFactory.Gm(); return ImageRendererFactory.Gm();
case ThumbnailProcessingLib.sharp: case ThumbnailProcessingLib.sharp:
return RendererFactory.Sharp(); return ImageRendererFactory.Sharp();
} }
throw new Error('unknown renderer'); throw new Error('unknown renderer');
} }
@ -46,8 +106,8 @@ export class RendererFactory {
const Jimp = require('jimp'); const Jimp = require('jimp');
return async (input: RendererInput): Promise<void> => { return async (input: RendererInput): Promise<void> => {
// generate thumbnail // generate thumbnail
Logger.silly('[JimpThRenderer] rendering thumbnail:' + input.imagePath); Logger.silly('[JimpThRenderer] rendering thumbnail:' + input.mediaPath);
const image = await Jimp.read(input.imagePath); const image = await Jimp.read(input.mediaPath);
/** /**
* newWidth * newHeight = size*size * newWidth * newHeight = size*size
* newHeight/newWidth = height/width * newHeight/newWidth = height/width
@ -86,8 +146,8 @@ export class RendererFactory {
const sharp = require('sharp'); const sharp = require('sharp');
return async (input: RendererInput): Promise<void> => { return async (input: RendererInput): Promise<void> => {
Logger.silly('[SharpThRenderer] rendering thumbnail:' + input.imagePath); Logger.silly('[SharpThRenderer] rendering thumbnail:' + input.mediaPath);
const image: Sharp = sharp(input.imagePath); const image: Sharp = sharp(input.mediaPath);
const metadata: Metadata = await image.metadata(); const metadata: Metadata = await image.metadata();
/** /**
@ -124,8 +184,8 @@ export class RendererFactory {
const gm = require('gm'); const gm = require('gm');
return (input: RendererInput): Promise<void> => { return (input: RendererInput): Promise<void> => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
Logger.silly('[GMThRenderer] rendering thumbnail:' + input.imagePath); Logger.silly('[GMThRenderer] rendering thumbnail:' + input.mediaPath);
let image: State = gm(input.imagePath); let image: State = gm(input.mediaPath);
image.size((err, value: Dimensions) => { image.size((err, value: Dimensions) => {
if (err) { if (err) {
return reject(err); return reject(err);

View File

@ -3,13 +3,16 @@ import {GalleryMWs} from '../middlewares/GalleryMWs';
import {RenderingMWs} from '../middlewares/RenderingMWs'; import {RenderingMWs} from '../middlewares/RenderingMWs';
import {ThumbnailGeneratorMWs} from '../middlewares/thumbnail/ThumbnailGeneratorMWs'; import {ThumbnailGeneratorMWs} from '../middlewares/thumbnail/ThumbnailGeneratorMWs';
import {UserRoles} from '../../common/entities/UserDTO'; import {UserRoles} from '../../common/entities/UserDTO';
import {ThumbnailSourceType} from '../model/threading/ThumbnailWorker';
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.addGetVideoThumbnail(app);
this.addGetImage(app); this.addGetImage(app);
this.addGetVideo(app);
this.addRandom(app); this.addRandom(app);
this.addDirectoryList(app); this.addDirectoryList(app);
@ -31,10 +34,19 @@ export class GalleryRouter {
private static addGetImage(app) { private static addGetImage(app) {
app.get(['/api/gallery/content/:imagePath(*\.(jpg|bmp|png|gif|jpeg))'], app.get(['/api/gallery/content/:mediaPath(*\.(jpg|bmp|png|gif|jpeg))'],
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
// TODO: authorize path // TODO: authorize path
GalleryMWs.loadImage, GalleryMWs.loadMedia,
RenderingMWs.renderFile
);
}
private static addGetVideo(app) {
app.get(['/api/gallery/content/:mediaPath(*\.(mp4))'],
AuthenticationMWs.authenticate,
// TODO: authorize path
GalleryMWs.loadMedia,
RenderingMWs.renderFile RenderingMWs.renderFile
); );
} }
@ -45,27 +57,37 @@ export class GalleryRouter {
AuthenticationMWs.authorise(UserRoles.Guest), AuthenticationMWs.authorise(UserRoles.Guest),
// TODO: authorize path // TODO: authorize path
GalleryMWs.getRandomImage, GalleryMWs.getRandomImage,
GalleryMWs.loadImage, GalleryMWs.loadMedia,
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/:mediaPath(*\.(jpg|bmp|png|gif|jpeg))/thumbnail/:size?',
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
// TODO: authorize path // TODO: authorize path
GalleryMWs.loadImage, GalleryMWs.loadMedia,
ThumbnailGeneratorMWs.generateThumbnail, ThumbnailGeneratorMWs.generateThumbnailFactory(ThumbnailSourceType.Image),
RenderingMWs.renderFile
);
}
private static addGetVideoThumbnail(app) {
app.get('/api/gallery/content/:mediaPath(*\.(mp4))/thumbnail/:size?',
AuthenticationMWs.authenticate,
// TODO: authorize path
GalleryMWs.loadMedia,
ThumbnailGeneratorMWs.generateThumbnailFactory(ThumbnailSourceType.Video),
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/:mediaPath(*\.(jpg|bmp|png|gif|jpeg))/icon',
AuthenticationMWs.authenticate, AuthenticationMWs.authenticate,
// TODO: authorize path // TODO: authorize path
GalleryMWs.loadImage, GalleryMWs.loadMedia,
ThumbnailGeneratorMWs.generateIcon, ThumbnailGeneratorMWs.generateIconFactory(ThumbnailSourceType.Image),
RenderingMWs.renderFile RenderingMWs.renderFile
); );
} }

View File

@ -1,4 +1,4 @@
import {PhotoDTO} from './PhotoDTO'; import {MediaDTO} from './MediaDTO';
export interface DirectoryDTO { export interface DirectoryDTO {
id: number; id: number;
@ -9,12 +9,12 @@ export interface DirectoryDTO {
isPartial?: boolean; isPartial?: boolean;
parent: DirectoryDTO; parent: DirectoryDTO;
directories: Array<DirectoryDTO>; directories: Array<DirectoryDTO>;
photos: Array<PhotoDTO>; media: MediaDTO[];
} }
export module DirectoryDTO { export module DirectoryDTO {
export const addReferences = (dir: DirectoryDTO): void => { export const addReferences = (dir: DirectoryDTO): void => {
dir.photos.forEach((photo: PhotoDTO) => { dir.media.forEach((photo: MediaDTO) => {
photo.directory = dir; photo.directory = dir;
}); });

View File

@ -0,0 +1,78 @@
import {DirectoryDTO} from './DirectoryDTO';
import {PhotoDTO} from './PhotoDTO';
import {OrientationTypes} from 'ts-exif-parser';
export interface MediaDTO {
id: number;
name: string;
directory: DirectoryDTO;
metadata: MediaMetadata;
readyThumbnails: Array<number>;
readyIcon: boolean;
}
export interface MediaMetadata {
keywords: string[];
positionData: PositionMetaData;
size: MediaDimension;
creationDate: number;
fileSize: number;
}
export interface PositionMetaData {
GPSData?: GPSMetadata;
country?: string;
state?: string;
city?: string;
}
export interface GPSMetadata {
latitude?: number;
longitude?: number;
altitude?: number;
}
export interface MediaDimension {
width: number;
height: number;
}
export module MediaDTO {
export const hasPositionData = (media: MediaDTO): boolean => {
return !!media.metadata.positionData &&
!!(media.metadata.positionData.city ||
media.metadata.positionData.state ||
media.metadata.positionData.country ||
(media.metadata.positionData.GPSData &&
media.metadata.positionData.GPSData.altitude &&
media.metadata.positionData.GPSData.latitude &&
media.metadata.positionData.GPSData.longitude));
};
export const isSideWay = (media: MediaDTO): boolean => {
if (!(<PhotoDTO>media).metadata.orientation) {
return false;
}
const photo = <PhotoDTO>media;
return photo.metadata.orientation === OrientationTypes.LEFT_TOP ||
photo.metadata.orientation === OrientationTypes.RIGHT_TOP ||
photo.metadata.orientation === OrientationTypes.LEFT_BOTTOM ||
photo.metadata.orientation === OrientationTypes.RIGHT_BOTTOM;
};
export const getRotatedSize = (photo: MediaDTO): MediaDimension => {
if (isSideWay(photo)) {
// noinspection JSSuspiciousNameCombination
return {width: photo.metadata.size.height, height: photo.metadata.size.width};
}
return photo.metadata.size;
};
export const calcRotatedAspectRatio = (photo: MediaDTO): number => {
const size = getRotatedSize(photo);
return size.width / size.height;
};
}

View File

@ -1,8 +1,8 @@
import {DirectoryDTO} from './DirectoryDTO'; import {DirectoryDTO} from './DirectoryDTO';
import {ImageSize} from './PhotoDTO';
import {OrientationTypes} from 'ts-exif-parser'; import {OrientationTypes} from 'ts-exif-parser';
import {MediaDTO, MediaMetadata, MediaDimension, PositionMetaData} from './MediaDTO';
export interface PhotoDTO { export interface PhotoDTO extends MediaDTO {
id: number; id: number;
name: string; name: string;
directory: DirectoryDTO; directory: DirectoryDTO;
@ -11,21 +11,16 @@ export interface PhotoDTO {
readyIcon: boolean; readyIcon: boolean;
} }
export interface PhotoMetadata { export interface PhotoMetadata extends MediaMetadata {
keywords: Array<string>; keywords: Array<string>;
cameraData: CameraMetadata; cameraData: CameraMetadata;
positionData: PositionMetaData; positionData: PositionMetaData;
orientation: OrientationTypes; orientation: OrientationTypes;
size: ImageSize; size: MediaDimension;
creationDate: number; creationDate: number;
fileSize: number; fileSize: number;
} }
export interface ImageSize {
width: number;
height: number;
}
export interface CameraMetadata { export interface CameraMetadata {
ISO?: number; ISO?: number;
model?: string; model?: string;
@ -35,50 +30,3 @@ export interface CameraMetadata {
focalLength?: number; focalLength?: number;
lens?: string; lens?: string;
} }
export interface PositionMetaData {
GPSData?: GPSMetadata;
country?: string;
state?: string;
city?: string;
}
export interface GPSMetadata {
latitude?: number;
longitude?: number;
altitude?: number;
}
export module PhotoDTO {
export const hasPositionData = (photo: PhotoDTO): boolean => {
return !!photo.metadata.positionData &&
!!(photo.metadata.positionData.city ||
photo.metadata.positionData.state ||
photo.metadata.positionData.country ||
(photo.metadata.positionData.GPSData &&
photo.metadata.positionData.GPSData.altitude &&
photo.metadata.positionData.GPSData.latitude &&
photo.metadata.positionData.GPSData.longitude));
};
export const isSideWay = (photo: PhotoDTO): boolean => {
return photo.metadata.orientation === OrientationTypes.LEFT_TOP ||
photo.metadata.orientation === OrientationTypes.RIGHT_TOP ||
photo.metadata.orientation === OrientationTypes.LEFT_BOTTOM ||
photo.metadata.orientation === OrientationTypes.RIGHT_BOTTOM;
};
export const getRotatedSize = (photo: PhotoDTO): ImageSize => {
if (isSideWay(photo)) {
// noinspection JSSuspiciousNameCombination
return {width: photo.metadata.size.height, height: photo.metadata.size.width};
}
return photo.metadata.size;
};
export const calcRotatedAspectRatio = (photo: PhotoDTO): number => {
const size = getRotatedSize(photo);
return size.width / size.height;
};
}

View File

@ -6,6 +6,6 @@ export interface SearchResultDTO {
searchText: string; searchText: string;
searchType: SearchTypes; searchType: SearchTypes;
directories: Array<DirectoryDTO>; directories: Array<DirectoryDTO>;
photos: Array<PhotoDTO>; media: Array<PhotoDTO>;
resultOverflow: boolean; resultOverflow: boolean;
} }

View File

@ -0,0 +1,9 @@
import {DirectoryDTO} from './DirectoryDTO';
import {MediaDTO, MediaMetadata} from './MediaDTO';
export interface VideoDTO extends MediaDTO {
id: number;
name: string;
directory: DirectoryDTO;
metadata: MediaMetadata;
}

View File

@ -2,7 +2,7 @@ import {Pipe, PipeTransform} from '@angular/core';
import {OrientationTypes} from 'ts-exif-parser'; import {OrientationTypes} from 'ts-exif-parser';
/** /**
* This pipe is used to fix thumbnail and photo orientation based on their exif orientation tag * This pipe is used to fix thumbnail and media orientation based on their exif orientation tag
*/ */
@Pipe({name: 'fixOrientation'}) @Pipe({name: 'fixOrientation'})

View File

@ -1,50 +0,0 @@
import {PhotoDTO} from '../../../common/entities/PhotoDTO';
import {Utils} from '../../../common/Utils';
import {Config} from '../../../common/config/public/Config';
export class IconPhoto {
protected replacementSizeCache: number | boolean = false;
constructor(public photo: PhotoDTO) {
}
iconLoaded() {
this.photo.readyIcon = true;
}
isIconAvailable() {
return this.photo.readyIcon;
}
getIconPath() {
return Utils.concatUrls(Config.Client.urlBase,
'/api/gallery/content/',
this.photo.directory.path, this.photo.directory.name, this.photo.name, 'icon');
}
getPhotoPath() {
return Utils.concatUrls(Config.Client.urlBase,
'/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

@ -1,20 +1,21 @@
import {PhotoDTO} from '../../../common/entities/PhotoDTO'; import {PhotoDTO} from '../../../common/entities/PhotoDTO';
import {Utils} from '../../../common/Utils'; import {Utils} from '../../../common/Utils';
import {IconPhoto} from './IconPhoto'; import {MediaIcon} from './MediaIcon';
import {Config} from '../../../common/config/public/Config'; import {Config} from '../../../common/config/public/Config';
import {MediaDTO} from '../../../common/entities/MediaDTO';
export class Photo extends IconPhoto { export class Media extends MediaIcon {
constructor(photo: PhotoDTO, public renderWidth: number, public renderHeight: number) { constructor(media: MediaDTO, public renderWidth: number, public renderHeight: number) {
super(photo); super(media);
} }
thumbnailLoaded() { thumbnailLoaded() {
if (!this.isThumbnailAvailable()) { if (!this.isThumbnailAvailable()) {
this.photo.readyThumbnails = this.photo.readyThumbnails || []; this.media.readyThumbnails = this.media.readyThumbnails || [];
this.photo.readyThumbnails.push(this.getThumbnailSize()); this.media.readyThumbnails.push(this.getThumbnailSize());
} }
} }
@ -29,10 +30,10 @@ export class Photo extends IconPhoto {
this.replacementSizeCache = null; this.replacementSizeCache = null;
const size = this.getThumbnailSize(); const size = this.getThumbnailSize();
if (!!this.photo.readyThumbnails) { if (!!this.media.readyThumbnails) {
for (let i = 0; i < this.photo.readyThumbnails.length; i++) { for (let i = 0; i < this.media.readyThumbnails.length; i++) {
if (this.photo.readyThumbnails[i] < size) { if (this.media.readyThumbnails[i] < size) {
this.replacementSizeCache = this.photo.readyThumbnails[i]; this.replacementSizeCache = this.media.readyThumbnails[i];
break; break;
} }
} }
@ -46,26 +47,26 @@ export class Photo extends IconPhoto {
} }
isThumbnailAvailable() { isThumbnailAvailable() {
return this.photo.readyThumbnails && this.photo.readyThumbnails.indexOf(this.getThumbnailSize()) !== -1; return this.media.readyThumbnails && this.media.readyThumbnails.indexOf(this.getThumbnailSize()) !== -1;
} }
getReplacementThumbnailPath() { getReplacementThumbnailPath() {
const size = this.getReplacementThumbnailSize(); const size = this.getReplacementThumbnailSize();
return Utils.concatUrls(Config.Client.urlBase, return Utils.concatUrls(Config.Client.urlBase,
'/api/gallery/content/', '/api/gallery/content/',
this.photo.directory.path, this.photo.directory.name, this.photo.name, 'thumbnail', size.toString()); this.media.directory.path, this.media.directory.name, this.media.name, 'thumbnail', size.toString());
} }
hasPositionData(): boolean { hasPositionData(): boolean {
return PhotoDTO.hasPositionData(this.photo); return MediaDTO.hasPositionData(this.media);
} }
getThumbnailPath() { getThumbnailPath() {
const size = this.getThumbnailSize(); const size = this.getThumbnailSize();
return Utils.concatUrls(Config.Client.urlBase, return Utils.concatUrls(Config.Client.urlBase,
'/api/gallery/content/', '/api/gallery/content/',
this.photo.directory.path, this.photo.directory.name, this.photo.name, 'thumbnail', size.toString()); this.media.directory.path, this.media.directory.name, this.media.name, 'thumbnail', size.toString());
} }

View File

@ -0,0 +1,51 @@
import {PhotoDTO} from '../../../common/entities/PhotoDTO';
import {Utils} from '../../../common/Utils';
import {Config} from '../../../common/config/public/Config';
import {MediaDTO} from '../../../common/entities/MediaDTO';
export class MediaIcon {
protected replacementSizeCache: number | boolean = false;
constructor(public media: MediaDTO) {
}
iconLoaded() {
this.media.readyIcon = true;
}
isIconAvailable() {
return this.media.readyIcon;
}
getIconPath() {
return Utils.concatUrls(Config.Client.urlBase,
'/api/gallery/content/',
this.media.directory.path, this.media.directory.name, this.media.name, 'icon');
}
getPhotoPath() {
return Utils.concatUrls(Config.Client.urlBase,
'/api/gallery/content/',
this.media.directory.path, this.media.directory.name, this.media.name);
}
equals(other: PhotoDTO | MediaIcon): boolean {
// is gridphoto
if (other instanceof MediaIcon) {
return this.media.directory.path === other.media.directory.path &&
this.media.directory.name === other.media.directory.name && this.media.name === other.media.name;
}
// is media
if (other.directory) {
return this.media.directory.path === other.directory.path &&
this.media.directory.name === other.directory.name && this.media.name === other.name;
}
return false;
}
}

View File

@ -5,6 +5,7 @@ import {Utils} from '../../../common/Utils';
import {Config} from '../../../common/config/public/Config'; import {Config} from '../../../common/config/public/Config';
import {AutoCompleteItem, SearchTypes} from '../../../common/entities/AutoCompleteItem'; import {AutoCompleteItem, SearchTypes} from '../../../common/entities/AutoCompleteItem';
import {SearchResultDTO} from '../../../common/entities/SearchResultDTO'; import {SearchResultDTO} from '../../../common/entities/SearchResultDTO';
import {MediaDTO} from '../../../common/entities/MediaDTO';
interface CacheItem<T> { interface CacheItem<T> {
timestamp: number; timestamp: number;
@ -132,24 +133,24 @@ export class GalleryCacheService {
} }
/** /**
* Update photo state at cache too (Eg.: thumbnail rendered) * Update media state at cache too (Eg.: thumbnail rendered)
* @param photo * @param media
*/ */
public photoUpdated(photo: PhotoDTO): void { public mediaUpdated(media: MediaDTO): void {
if (Config.Client.Other.enableCache === false) { if (Config.Client.Other.enableCache === false) {
return; return;
} }
const directoryName = Utils.concatUrls(photo.directory.path, photo.directory.name); const directoryName = Utils.concatUrls(media.directory.path, media.directory.name);
const value = localStorage.getItem(directoryName); const value = localStorage.getItem(directoryName);
if (value != null) { if (value != null) {
const directory: DirectoryDTO = JSON.parse(value); const directory: DirectoryDTO = JSON.parse(value);
directory.photos.forEach((p) => { directory.media.forEach((p) => {
if (p.name === photo.name) { if (p.name === media.name) {
// update data // update data
p.metadata = photo.metadata; p.metadata = media.metadata;
p.readyThumbnails = photo.readyThumbnails; p.readyThumbnails = media.readyThumbnails;
// save changes // save changes
localStorage.setItem(directoryName, JSON.stringify(directory)); localStorage.setItem(directoryName, JSON.stringify(directory));

View File

@ -55,7 +55,7 @@ a:hover .photo-container {
white-space: normal; white-space: normal;
} }
/* transforming photo, based on exif orientation*/ /* transforming media, based on exif orientation*/
.photo-orientation-1 { .photo-orientation-1 {
} }
.photo-orientation-2 { .photo-orientation-2 {

View File

@ -3,11 +3,12 @@ import {DomSanitizer} from '@angular/platform-browser';
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';
import {Photo} from '../Photo'; import {Media} from '../Media';
import {Thumbnail, ThumbnailManagerService} from '../thumnailManager.service'; import {Thumbnail, ThumbnailManagerService} from '../thumnailManager.service';
import {PageHelper} from '../../model/page.helper'; import {PageHelper} from '../../model/page.helper';
import {QueryService} from '../../model/query.service'; import {QueryService} from '../../model/query.service';
import {PhotoDTO} from '../../../../common/entities/PhotoDTO'; import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
import {MediaDTO} from '../../../../common/entities/MediaDTO';
@Component({ @Component({
selector: 'app-gallery-directory', selector: 'app-gallery-directory',
@ -28,9 +29,9 @@ export class GalleryDirectoryComponent implements OnInit, OnDestroy {
size: number = null; size: number = null;
public get SamplePhoto(): PhotoDTO { public get SamplePhoto(): MediaDTO {
if (this.directory.photos.length > 0) { if (this.directory.media.length > 0) {
return this.directory.photos[0]; return this.directory.media[0];
} }
return null; return null;
} }
@ -57,8 +58,8 @@ export class GalleryDirectoryComponent implements OnInit, OnDestroy {
} }
ngOnInit() { ngOnInit() {
if (this.directory.photos.length > 0) { if (this.directory.media.length > 0) {
this.thumbnail = this.thumbnailService.getThumbnail(new Photo(this.SamplePhoto, this.calcSize(), this.calcSize())); this.thumbnail = this.thumbnailService.getThumbnail(new Media(this.SamplePhoto, this.calcSize(), this.calcSize()));
} }
} }

View File

@ -34,8 +34,8 @@
[directory]="directory"></app-gallery-directory> [directory]="directory"></app-gallery-directory>
</div> </div>
<app-gallery-map *ngIf="isPhotoWithLocation && mapEnabled" <app-gallery-map *ngIf="isPhotoWithLocation && mapEnabled"
[photos]="_galleryService.content.value.directory.photos"></app-gallery-map> [photos]="_galleryService.content.value.directory.media"></app-gallery-map>
<app-gallery-grid [photos]="_galleryService.content.value.directory.photos" <app-gallery-grid [photos]="_galleryService.content.value.directory.media"
[lightbox]="lightbox"></app-gallery-grid> [lightbox]="lightbox"></app-gallery-grid>
</div> </div>
@ -58,13 +58,13 @@
</ol> </ol>
<app-gallery-map *ngIf="isPhotoWithLocation && mapEnabled" <app-gallery-map *ngIf="isPhotoWithLocation && mapEnabled"
[photos]="_galleryService.content.value.searchResult.photos"></app-gallery-map> [photos]="_galleryService.content.value.searchResult.media"></app-gallery-map>
<div class="directories"> <div class="directories">
<app-gallery-directory *ngFor="let directory of directories" <app-gallery-directory *ngFor="let directory of directories"
[directory]="directory"></app-gallery-directory> [directory]="directory"></app-gallery-directory>
</div> </div>
<app-gallery-grid [photos]="_galleryService.content.value.searchResult.photos" <app-gallery-grid [photos]="_galleryService.content.value.searchResult.media"
[lightbox]="lightbox"></app-gallery-grid> [lightbox]="lightbox"></app-gallery-grid>
</div> </div>

View File

@ -122,16 +122,16 @@ export class GalleryComponent implements OnInit, OnDestroy {
const tmp = <DirectoryDTO | SearchResultDTO>(content.searchResult || content.directory || { const tmp = <DirectoryDTO | SearchResultDTO>(content.searchResult || content.directory || {
directories: [], directories: [],
photos: [] media: []
}); });
this.directories = tmp.directories; this.directories = tmp.directories;
this.sortDirectories(); this.sortDirectories();
this.isPhotoWithLocation = false; this.isPhotoWithLocation = false;
for (let i = 0; i < tmp.photos.length; i++) { for (let i = 0; i < tmp.media.length; i++) {
if (tmp.photos[i].metadata && if (tmp.media[i].metadata &&
tmp.photos[i].metadata.positionData && tmp.media[i].metadata.positionData &&
tmp.photos[i].metadata.positionData.GPSData && tmp.media[i].metadata.positionData.GPSData &&
tmp.photos[i].metadata.positionData.GPSData.longitude tmp.media[i].metadata.positionData.GPSData.longitude
) { ) {
this.isPhotoWithLocation = true; this.isPhotoWithLocation = true;
break; break;

View File

@ -147,7 +147,7 @@ export class GalleryService {
this.content.next(cw); this.content.next(cw);
// if instant search do not have a result, do not do a search // if instant search do not have a result, do not do a search
if (cw.searchResult.photos.length === 0 && cw.searchResult.directories.length === 0) { if (cw.searchResult.media.length === 0 && cw.searchResult.directories.length === 0) {
if (this.searchId != null) { if (this.searchId != null) {
clearTimeout(this.searchId); clearTimeout(this.searchId);
} }

View File

@ -0,0 +1,26 @@
import {Media} from '../Media';
import {MediaDTO} from '../../../../common/entities/MediaDTO';
import {OrientationTypes} from 'ts-exif-parser';
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
export class GridMedia extends Media {
constructor(media: MediaDTO, renderWidth: number, renderHeight: number, public rowId: number) {
super(media, renderWidth, renderHeight);
}
public get Orientation(): OrientationTypes {
return (<PhotoDTO>this.media).metadata.orientation || OrientationTypes.TOP_LEFT;
}
isPhoto(): boolean {
return typeof (<PhotoDTO>this.media).metadata.cameraData !== 'undefined';
}
isVideo(): boolean {
return typeof (<PhotoDTO>this.media).metadata.cameraData === 'undefined';
}
}

View File

@ -1,12 +0,0 @@
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
import {Photo} from '../Photo';
export class GridPhoto extends Photo {
constructor(photo: PhotoDTO, renderWidth: number, renderHeight: number, public rowId: number) {
super(photo, renderWidth, renderHeight);
}
}

View File

@ -1,10 +1,11 @@
import {PhotoDTO} from '../../../../common/entities/PhotoDTO'; import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
import {MediaDTO} from '../../../../common/entities/MediaDTO';
export class GridRowBuilder { export class GridRowBuilder {
private photoRow: PhotoDTO[] = []; private photoRow: PhotoDTO[] = [];
private photoIndex = 0; // index of the last pushed photo to the photoRow private photoIndex = 0; // index of the last pushed media to the photoRow
constructor(private photos: PhotoDTO[], constructor(private photos: PhotoDTO[],
@ -52,7 +53,7 @@ export class GridRowBuilder {
while (this.calcRowHeight() < minHeight && this.removePhoto() === true) { // roo too small -> remove images while (this.calcRowHeight() < minHeight && this.removePhoto() === true) { // roo too small -> remove images
} }
// keep at least one photo int thr row // keep at least one media int thr row
if (this.photoRow.length <= 0) { if (this.photoRow.length <= 0) {
this.addPhoto(); this.addPhoto();
} }
@ -61,7 +62,7 @@ export class GridRowBuilder {
public calcRowHeight(): number { public calcRowHeight(): number {
let width = 0; let width = 0;
for (let i = 0; i < this.photoRow.length; i++) { for (let i = 0; i < this.photoRow.length; i++) {
const size = PhotoDTO.getRotatedSize(this.photoRow[i]); const size = MediaDTO.getRotatedSize(this.photoRow[i]);
width += (size.width / size.height); // summing up aspect ratios width += (size.width / size.height); // summing up aspect ratios
} }
const height = (this.containerWidth - this.photoRow.length * (this.photoMargin * 2) - 1) / width; // cant be equal -> width-1 const height = (this.containerWidth - this.photoRow.length * (this.photoMargin * 2) - 1) / width; // cant be equal -> width-1

View File

@ -3,7 +3,7 @@
*ngFor="let gridPhoto of photosToRender" *ngFor="let gridPhoto of photosToRender"
[routerLink]="[]" [routerLink]="[]"
[queryParams]="queryService.getParams(gridPhoto.photo)" [queryParams]="queryService.getParams(gridPhoto.media)"
[gridPhoto]="gridPhoto" [gridPhoto]="gridPhoto"
[style.width.px]="gridPhoto.renderWidth" [style.width.px]="gridPhoto.renderWidth"
[style.height.px]="gridPhoto.renderHeight" [style.height.px]="gridPhoto.renderHeight"

View File

@ -15,7 +15,7 @@ import {
import {PhotoDTO} from '../../../../common/entities/PhotoDTO'; import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
import {GridRowBuilder} from './GridRowBuilder'; import {GridRowBuilder} from './GridRowBuilder';
import {GalleryLightboxComponent} from '../lightbox/lightbox.gallery.component'; import {GalleryLightboxComponent} from '../lightbox/lightbox.gallery.component';
import {GridPhoto} from './GridPhoto'; import {GridMedia} from './GridMedia';
import {GalleryPhotoComponent} from './photo/photo.grid.gallery.component'; import {GalleryPhotoComponent} from './photo/photo.grid.gallery.component';
import {OverlayService} from '../overlay.service'; import {OverlayService} from '../overlay.service';
import {Config} from '../../../../common/config/public/Config'; import {Config} from '../../../../common/config/public/Config';
@ -25,6 +25,7 @@ import {ActivatedRoute, Params, Router} from '@angular/router';
import {QueryService} from '../../model/query.service'; import {QueryService} from '../../model/query.service';
import {GalleryService} from '../gallery.service'; import {GalleryService} from '../gallery.service';
import {SortingMethods} from '../../../../common/entities/SortingMethods'; import {SortingMethods} from '../../../../common/entities/SortingMethods';
import {MediaDTO} from '../../../../common/entities/MediaDTO';
@Component({ @Component({
selector: 'app-gallery-grid', selector: 'app-gallery-grid',
@ -40,7 +41,7 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O
@Input() photos: Array<PhotoDTO>; @Input() photos: Array<PhotoDTO>;
@Input() lightbox: GalleryLightboxComponent; @Input() lightbox: GalleryLightboxComponent;
photosToRender: Array<GridPhoto> = []; photosToRender: Array<GridMedia> = [];
containerWidth = 0; containerWidth = 0;
screenHeight = 0; screenHeight = 0;
@ -189,8 +190,8 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O
const imageHeight = rowHeight - (this.IMAGE_MARGIN * 2); const imageHeight = rowHeight - (this.IMAGE_MARGIN * 2);
photoRowBuilder.getPhotoRow().forEach((photo) => { photoRowBuilder.getPhotoRow().forEach((photo) => {
const imageWidth = imageHeight * PhotoDTO.calcRotatedAspectRatio(photo); const imageWidth = imageHeight * MediaDTO.calcRotatedAspectRatio(photo);
this.photosToRender.push(new GridPhoto(photo, imageWidth, imageHeight, this.renderedPhotoIndex)); this.photosToRender.push(new GridMedia(photo, imageWidth, imageHeight, this.renderedPhotoIndex));
}); });
this.renderedPhotoIndex += photoRowBuilder.getPhotoRow().length; this.renderedPhotoIndex += photoRowBuilder.getPhotoRow().length;
@ -248,7 +249,7 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O
let lastRowId = null; let lastRowId = null;
for (let i = 0; i < this.photos.length && i < this.photosToRender.length; ++i) { for (let i = 0; i < this.photos.length && i < this.photosToRender.length; ++i) {
// If a photo changed the whole row has to be removed // If a media changed the whole row has to be removed
if (this.photosToRender[i].rowId !== lastRowId) { if (this.photosToRender[i].rowId !== lastRowId) {
lastSameIndex = i; lastSameIndex = i;
lastRowId = this.photosToRender[i].rowId; lastRowId = this.photosToRender[i].rowId;
@ -316,7 +317,7 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O
this.renderedPhotoIndex < numberOfPhotos)) { this.renderedPhotoIndex < numberOfPhotos)) {
const ret = this.renderARow(); const ret = this.renderARow();
if (ret === null) { if (ret === null) {
throw new Error('Grid photos rendering failed'); throw new Error('Grid media rendering failed');
} }
renderedContentHeight += ret; renderedContentHeight += ret;
} }

View File

@ -1,5 +1,5 @@
<div #photoContainer class="photo-container" (mouseover)="mouseOver()" (mouseout)="mouseOut()"> <div #photoContainer class="photo-container" (mouseover)="mouseOver()" (mouseout)="mouseOut()">
<img #img [src]="thumbnail.Src | fixOrientation:gridPhoto.photo.metadata.orientation | async" <img #img [src]="thumbnail.Src | fixOrientation:gridPhoto.media.metadata.orientation | async"
*ngIf="thumbnail.Available"> *ngIf="thumbnail.Available">
<app-gallery-grid-photo-loading <app-gallery-grid-photo-loading
@ -14,7 +14,7 @@
[style.margin-top.px]="infoBar.marginTop" [style.margin-top.px]="infoBar.marginTop"
[style.background]="infoBar.background" [style.background]="infoBar.background"
[style.width.px]="container.nativeElement.offsetWidth"> [style.width.px]="container.nativeElement.offsetWidth">
<div class="photo-name">{{gridPhoto.photo.name}}</div> <div class="photo-name">{{gridPhoto.media.name}}</div>
<div class="photo-position" *ngIf="gridPhoto.hasPositionData()"> <div class="photo-position" *ngIf="gridPhoto.hasPositionData()">
<span class="oi oi-map-marker"></span> <span class="oi oi-map-marker"></span>
@ -27,8 +27,8 @@
</ng-template> </ng-template>
</div> </div>
<div class="photo-keywords" *ngIf="gridPhoto.photo.metadata.keywords && gridPhoto.photo.metadata.keywords.length"> <div class="photo-keywords" *ngIf="gridPhoto.media.metadata.keywords && gridPhoto.media.metadata.keywords.length">
<ng-template ngFor let-keyword [ngForOf]="gridPhoto.photo.metadata.keywords" let-last="last"> <ng-template ngFor let-keyword [ngForOf]="gridPhoto.media.metadata.keywords" let-last="last">
<a *ngIf="searchEnabled" <a *ngIf="searchEnabled"
[routerLink]="['/search', keyword, {type: SearchTypes[SearchTypes.keyword]}]">#{{keyword}}</a> [routerLink]="['/search', keyword, {type: SearchTypes[SearchTypes.keyword]}]">#{{keyword}}</a>
<span *ngIf="!searchEnabled">#{{keyword}}</span> <span *ngIf="!searchEnabled">#{{keyword}}</span>

View File

@ -1,6 +1,6 @@
import {Component, ElementRef, Input, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {Component, ElementRef, Input, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {Dimension, IRenderable} from '../../../model/IRenderable'; import {Dimension, IRenderable} from '../../../model/IRenderable';
import {GridPhoto} from '../GridPhoto'; import {GridMedia} from '../GridMedia';
import {SearchTypes} from '../../../../../common/entities/AutoCompleteItem'; import {SearchTypes} from '../../../../../common/entities/AutoCompleteItem';
import {RouterLink} from '@angular/router'; import {RouterLink} from '@angular/router';
import {Thumbnail, ThumbnailManagerService} from '../../thumnailManager.service'; import {Thumbnail, ThumbnailManagerService} from '../../thumnailManager.service';
@ -15,7 +15,7 @@ import {PageHelper} from '../../../model/page.helper';
providers: [RouterLink] providers: [RouterLink]
}) })
export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy { export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy {
@Input() gridPhoto: GridPhoto; @Input() gridPhoto: GridMedia;
@ViewChild('img') imageRef: ElementRef; @ViewChild('img') imageRef: ElementRef;
@ViewChild('info') infoDiv: ElementRef; @ViewChild('info') infoDiv: ElementRef;
@ViewChild('photoContainer') container: ElementRef; @ViewChild('photoContainer') container: ElementRef;
@ -78,9 +78,9 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy {
if (!this.gridPhoto) { if (!this.gridPhoto) {
return ''; return '';
} }
return this.gridPhoto.photo.metadata.positionData.city || return this.gridPhoto.media.metadata.positionData.city ||
this.gridPhoto.photo.metadata.positionData.state || this.gridPhoto.media.metadata.positionData.state ||
this.gridPhoto.photo.metadata.positionData.country; this.gridPhoto.media.metadata.positionData.country;
} }

View File

@ -11,12 +11,12 @@
</div> </div>
<div class="col-10"> <div class="col-10">
<div class="details-main"> <div class="details-main">
{{photo.name}} {{media.name}}
</div> </div>
<div class="details-sub row"> <div class="details-sub row">
<div class="col-4">{{photo.metadata.size.width}} x {{photo.metadata.size.height}}</div> <div class="col-4">{{media.metadata.size.width}} x {{media.metadata.size.height}}</div>
<div class="col-4">{{calcMpx()}}MP</div> <div class="col-4">{{calcMpx()}}MP</div>
<div class="col-4" *ngIf="photo.metadata.fileSize">{{calcFileSize()}}</div> <div class="col-4" *ngIf="media.metadata.fileSize">{{calcFileSize()}}</div>
</div> </div>
</div> </div>
</div> </div>
@ -27,32 +27,32 @@
</div> </div>
<div class="col-10"> <div class="col-10">
<div class="details-main"> <div class="details-main">
{{ photo.metadata.creationDate | date: (isThisYear() ? 'MMMM d': 'longDate')}} {{ media.metadata.creationDate | date: (isThisYear() ? 'MMMM d': 'longDate')}}
</div> </div>
<div class="details-sub row"> <div class="details-sub row">
<div class="col-12">{{ photo.metadata.creationDate | date :'EEEE'}}, {{getTime()}}</div> <div class="col-12">{{ media.metadata.creationDate | date :'EEEE'}}, {{getTime()}}</div>
</div> </div>
</div> </div>
</div> </div>
<div class="row"> <div class="row" *ngIf="CameraData">
<div class="col-2"> <div class="col-2">
<span class="details-icon oi oi-camera-slr"></span> <span class="details-icon oi oi-camera-slr"></span>
</div> </div>
<div class="col-10"> <div class="col-10">
<div class="details-main"> <div class="details-main">
{{photo.metadata.cameraData.model || photo.metadata.cameraData.make || "Camera"}} {{CameraData.model || CameraData.make || "Camera"}}
</div> </div>
<div class="details-sub row"> <div class="details-sub row">
<div class="col-3" *ngIf="photo.metadata.cameraData.ISO">ISO{{photo.metadata.cameraData.ISO}}</div> <div class="col-3" *ngIf="CameraData.ISO">ISO{{CameraData.ISO}}</div>
<div class="col-3" *ngIf="photo.metadata.cameraData.fStop">f/{{photo.metadata.cameraData.fStop}}</div> <div class="col-3" *ngIf="CameraData.fStop">f/{{CameraData.fStop}}</div>
<div class="col-3" *ngIf="photo.metadata.cameraData.exposure"> <div class="col-3" *ngIf="CameraData.exposure">
{{toFraction(photo.metadata.cameraData.exposure)}}s {{toFraction(CameraData.exposure)}}s
</div> </div>
<div class="col-3" *ngIf="photo.metadata.cameraData.focalLength"> <div class="col-3" *ngIf="CameraData.focalLength">
{{photo.metadata.cameraData.focalLength}}mm {{CameraData.focalLength}}mm
</div> </div>
<div class="col-12" *ngIf="photo.metadata.cameraData.lens">{{photo.metadata.cameraData.lens}}</div> <div class="col-12" *ngIf="CameraData.lens">{{CameraData.lens}}</div>
</div> </div>
</div> </div>
</div> </div>
@ -67,8 +67,8 @@
</div> </div>
<div class="details-sub row" *ngIf="hasGPS()"> <div class="details-sub row" *ngIf="hasGPS()">
<div class="col-12"> <div class="col-12">
{{photo.metadata.positionData.GPSData.latitude.toFixed(3)}}, {{media.metadata.positionData.GPSData.latitude.toFixed(3)}},
{{photo.metadata.positionData.GPSData.longitude.toFixed(3)}} {{media.metadata.positionData.GPSData.longitude.toFixed(3)}}
</div> </div>
</div> </div>
</div> </div>
@ -80,11 +80,11 @@
[zoomControl]="false" [zoomControl]="false"
[streetViewControl]="false" [streetViewControl]="false"
[zoom]="10" [zoom]="10"
[latitude]="photo.metadata.positionData.GPSData.latitude" [latitude]="media.metadata.positionData.GPSData.latitude"
[longitude]="photo.metadata.positionData.GPSData.longitude"> [longitude]="media.metadata.positionData.GPSData.longitude">
<agm-marker <agm-marker
[latitude]="photo.metadata.positionData.GPSData.latitude" [latitude]="media.metadata.positionData.GPSData.latitude"
[longitude]="photo.metadata.positionData.GPSData.longitude"> [longitude]="media.metadata.positionData.GPSData.longitude">
</agm-marker> </agm-marker>
</agm-map> </agm-map>
</div> </div>

View File

@ -1,6 +1,7 @@
import {Component, ElementRef, EventEmitter, Input, Output} from '@angular/core'; import {Component, ElementRef, EventEmitter, Input, Output} from '@angular/core';
import {PhotoDTO} from '../../../../../common/entities/PhotoDTO'; import {CameraMetadata, PhotoDTO} from '../../../../../common/entities/PhotoDTO';
import {Config} from '../../../../../common/config/public/Config'; import {Config} from '../../../../../common/config/public/Config';
import {MediaDTO} from '../../../../../common/entities/MediaDTO';
@Component({ @Component({
selector: 'app-info-panel', selector: 'app-info-panel',
@ -8,7 +9,7 @@ import {Config} from '../../../../../common/config/public/Config';
templateUrl: './info-panel.lightbox.gallery.component.html', templateUrl: './info-panel.lightbox.gallery.component.html',
}) })
export class InfoPanelLightboxComponent { export class InfoPanelLightboxComponent {
@Input() photo: PhotoDTO; @Input() media: MediaDTO;
@Output('onClose') onClose = new EventEmitter(); @Output('onClose') onClose = new EventEmitter();
public mapEnabled = true; public mapEnabled = true;
@ -17,13 +18,13 @@ export class InfoPanelLightboxComponent {
} }
calcMpx() { calcMpx() {
return (this.photo.metadata.size.width * this.photo.metadata.size.height / 1000000).toFixed(2); return (this.media.metadata.size.width * this.media.metadata.size.height / 1000000).toFixed(2);
} }
calcFileSize() { calcFileSize() {
const postFixes = ['B', 'KB', 'MB', 'GB', 'TB']; const postFixes = ['B', 'KB', 'MB', 'GB', 'TB'];
let index = 0; let index = 0;
let size = this.photo.metadata.fileSize; let size = this.media.metadata.fileSize;
while (size > 1000 && index < postFixes.length - 1) { while (size > 1000 && index < postFixes.length - 1) {
size /= 1000; size /= 1000;
index++; index++;
@ -33,12 +34,12 @@ export class InfoPanelLightboxComponent {
isThisYear() { isThisYear() {
return (new Date()).getFullYear() === return (new Date()).getFullYear() ===
(new Date(this.photo.metadata.creationDate)).getFullYear(); (new Date(this.media.metadata.creationDate)).getFullYear();
} }
getTime() { getTime() {
const date = new Date(this.photo.metadata.creationDate); const date = new Date(this.media.metadata.creationDate);
return date.toTimeString().split(' ')[0]; return date.toTimeString().split(' ')[0];
} }
@ -51,29 +52,33 @@ export class InfoPanelLightboxComponent {
} }
hasPositionData(): boolean { hasPositionData(): boolean {
return PhotoDTO.hasPositionData(this.photo); return MediaDTO.hasPositionData(this.media);
} }
hasGPS() { hasGPS() {
return this.photo.metadata.positionData && this.photo.metadata.positionData.GPSData && return this.media.metadata.positionData && this.media.metadata.positionData.GPSData &&
this.photo.metadata.positionData.GPSData.latitude && this.photo.metadata.positionData.GPSData.longitude; this.media.metadata.positionData.GPSData.latitude && this.media.metadata.positionData.GPSData.longitude;
} }
getPositionText(): string { getPositionText(): string {
if (!this.photo.metadata.positionData) { if (!this.media.metadata.positionData) {
return ''; return '';
} }
let str = this.photo.metadata.positionData.city || let str = this.media.metadata.positionData.city ||
this.photo.metadata.positionData.state || ''; this.media.metadata.positionData.state || '';
if (str.length !== 0) { if (str.length !== 0) {
str += ', '; str += ', ';
} }
str += this.photo.metadata.positionData.country || ''; str += this.media.metadata.positionData.country || '';
return str; return str;
} }
get CameraData(): CameraMetadata {
return (<PhotoDTO>this.media).metadata.cameraData;
}
close() { close() {
this.onClose.emit(); this.onClose.emit();
} }

View File

@ -5,8 +5,8 @@
</div> </div>
<div class="lightbox" #lightbox> <div class="lightbox" #lightbox>
<app-gallery-lightbox-photo [gridPhoto]="activePhoto ? activePhoto.gridPhoto : null" <app-gallery-lightbox-photo [gridMedia]="activePhoto ? activePhoto.gridPhoto : null"
[loadImage]="!animating" [loadMedia]="!animating"
[windowAspect]="getWindowAspectRatio()" [windowAspect]="getWindowAspectRatio()"
#photo> #photo>
</app-gallery-lightbox-photo> </app-gallery-lightbox-photo>
@ -20,7 +20,7 @@
<a *ngIf="activePhoto" <a *ngIf="activePhoto"
class="highlight control-button" class="highlight control-button"
[href]="activePhoto.gridPhoto.getPhotoPath()" [href]="activePhoto.gridPhoto.getPhotoPath()"
[download]="activePhoto.gridPhoto.photo.name"> [download]="activePhoto.gridPhoto.media.name">
<span class="oi oi-data-transfer-download" <span class="oi oi-data-transfer-download"
title="download" i18n-title></span> title="download" i18n-title></span>
</a> </a>
@ -84,7 +84,7 @@
<app-info-panel *ngIf="activePhoto && infoPanelVisible" <app-info-panel *ngIf="activePhoto && infoPanelVisible"
id="info-panel" id="info-panel"
[style.width.px]="infoPanelWidth" [style.width.px]="infoPanelWidth"
[photo]="activePhoto.gridPhoto.photo" [media]="activePhoto.gridPhoto.photo"
(onClose)="hideInfoPanel()"> (onClose)="hideInfoPanel()">
</app-info-panel> </app-info-panel>

View File

@ -22,6 +22,7 @@ import {filter} from 'rxjs/operators';
import {ActivatedRoute, Params, Router} from '@angular/router'; import {ActivatedRoute, Params, Router} from '@angular/router';
import {PageHelper} from '../../model/page.helper'; import {PageHelper} from '../../model/page.helper';
import {QueryService} from '../../model/query.service'; import {QueryService} from '../../model/query.service';
import {MediaDTO} from '../../../../common/entities/MediaDTO';
export enum LightboxStates { export enum LightboxStates {
Open = 1, Open = 1,
@ -111,15 +112,15 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
} }
onNavigateTo(photoName: string) { onNavigateTo(photoName: string) {
if (this.activePhoto && this.activePhoto.gridPhoto.photo.name === photoName) { if (this.activePhoto && this.activePhoto.gridPhoto.media.name === photoName) {
return; return;
} }
const photo = this.gridPhotoQL.find(i => i.gridPhoto.photo.name === photoName); const photo = this.gridPhotoQL.find(i => i.gridPhoto.media.name === photoName);
if (!photo) { if (!photo) {
return this.delayedPhotoShow = photoName; return this.delayedPhotoShow = photoName;
} }
if (this.status === LightboxStates.Closed) { if (this.status === LightboxStates.Closed) {
this.showLigthbox(photo.gridPhoto.photo); this.showLigthbox(photo.gridPhoto.media);
} else { } else {
this.showPhoto(this.gridPhotoQL.toArray().indexOf(photo)); this.showPhoto(this.gridPhotoQL.toArray().indexOf(photo));
} }
@ -175,7 +176,7 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
private navigateToPhoto(photoIndex: number) { private navigateToPhoto(photoIndex: number) {
this.router.navigate([], this.router.navigate([],
{queryParams: this.queryService.getParams(this.gridPhotoQL.toArray()[photoIndex].gridPhoto.photo)}); {queryParams: this.queryService.getParams(this.gridPhotoQL.toArray()[photoIndex].gridPhoto.media)});
/* /*
this.activePhoto = null; this.activePhoto = null;
this.changeDetector.detectChanges(); this.changeDetector.detectChanges();
@ -188,7 +189,7 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
this.updateActivePhoto(photoIndex, resize); this.updateActivePhoto(photoIndex, resize);
} }
public showLigthbox(photo: PhotoDTO) { public showLigthbox(photo: MediaDTO) {
this.controllersVisible = true; this.controllersVisible = true;
this.showControls(); this.showControls();
this.status = LightboxStates.Open; this.status = LightboxStates.Open;
@ -200,7 +201,7 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
const lightboxDimension = selectedPhoto.getDimension(); const lightboxDimension = selectedPhoto.getDimension();
lightboxDimension.top -= PageHelper.ScrollY; lightboxDimension.top -= PageHelper.ScrollY;
this.animating = true; this.animating = true;
this.animatePhoto(selectedPhoto.getDimension(), this.calcLightBoxPhotoDimension(selectedPhoto.gridPhoto.photo)).onDone(() => { this.animatePhoto(selectedPhoto.getDimension(), this.calcLightBoxPhotoDimension(selectedPhoto.gridPhoto.media)).onDone(() => {
this.animating = false; this.animating = false;
}); });
this.animateLightbox( this.animateLightbox(
@ -261,7 +262,7 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
lightboxDimension.top -= PageHelper.ScrollY; lightboxDimension.top -= PageHelper.ScrollY;
this.blackCanvasOpacity = 0; this.blackCanvasOpacity = 0;
this.animatePhoto(this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.photo), this.activePhoto.getDimension()); this.animatePhoto(this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.media), this.activePhoto.getDimension());
this.animateLightbox(<Dimension>{ this.animateLightbox(<Dimension>{
top: 0, top: 0,
left: 0, left: 0,
@ -325,9 +326,9 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
this.infoPanelVisible = false; this.infoPanelVisible = false;
}, 1000); }, 1000);
const starPhotoPos = this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.photo); const starPhotoPos = this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.media);
this.infoPanelWidth = 0; this.infoPanelWidth = 0;
const endPhotoPos = this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.photo); const endPhotoPos = this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.media);
if (_animate) { if (_animate) {
this.animatePhoto(starPhotoPos, endPhotoPos); this.animatePhoto(starPhotoPos, endPhotoPos);
} }
@ -365,9 +366,9 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
showInfoPanel() { showInfoPanel() {
this.infoPanelVisible = true; this.infoPanelVisible = true;
const starPhotoPos = this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.photo); const starPhotoPos = this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.media);
this.infoPanelWidth = 400; this.infoPanelWidth = 400;
const endPhotoPos = this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.photo); const endPhotoPos = this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.media);
this.animatePhoto(starPhotoPos, endPhotoPos); this.animatePhoto(starPhotoPos, endPhotoPos);
this.animateLightbox(<Dimension>{ this.animateLightbox(<Dimension>{
top: 0, top: 0,
@ -419,13 +420,13 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
if (photoIndex < 0 || photoIndex > this.gridPhotoQL.length) { if (photoIndex < 0 || photoIndex > this.gridPhotoQL.length) {
throw new Error('Can\'t find the photo'); throw new Error('Can\'t find the media');
} }
this.activePhotoId = photoIndex; this.activePhotoId = photoIndex;
this.activePhoto = pcList[photoIndex]; this.activePhoto = pcList[photoIndex];
if (resize) { if (resize) {
this.animatePhoto(this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.photo)); this.animatePhoto(this.calcLightBoxPhotoDimension(this.activePhoto.gridPhoto.media));
} }
this.navigation.hasPrev = photoIndex > 0; this.navigation.hasPrev = photoIndex > 0;
this.navigation.hasNext = photoIndex + 1 < pcList.length; this.navigation.hasNext = photoIndex + 1 < pcList.length;
@ -466,17 +467,17 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit {
this.playBackState = 0; this.playBackState = 0;
} }
private findPhotoComponent(photo: any): GalleryPhotoComponent { private findPhotoComponent(media: MediaDTO): GalleryPhotoComponent {
const galleryPhotoComponents = this.gridPhotoQL.toArray(); const galleryPhotoComponents = this.gridPhotoQL.toArray();
for (let i = 0; i < galleryPhotoComponents.length; i++) { for (let i = 0; i < galleryPhotoComponents.length; i++) {
if (galleryPhotoComponents[i].gridPhoto.photo === photo) { if (galleryPhotoComponents[i].gridPhoto.media === media) {
return galleryPhotoComponents[i]; return galleryPhotoComponents[i];
} }
} }
return null; return null;
} }
private calcLightBoxPhotoDimension(photo: PhotoDTO): Dimension { private calcLightBoxPhotoDimension(photo: MediaDTO): Dimension {
let width = 0; let width = 0;
let height = 0; let height = 0;
const photoAspect = photo.metadata.size.width / photo.metadata.size.height; const photoAspect = photo.metadata.size.width / photo.metadata.size.height;

View File

@ -1,4 +1,5 @@
.imgContainer img { .imgContainer img,
.imgContainer video {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 50%; left: 50%;

View File

@ -4,13 +4,26 @@
[style.height.%]="imageSize.height" [style.height.%]="imageSize.height"
[src]="thumbnailSrc"/> [src]="thumbnailSrc"/>
<img *ngIf="gridPhoto !== null && loadImage && photoSrc" <img *ngIf="gridMedia !== null && gridMedia.isPhoto() && loadMedia && photoSrc"
[style.width.%]="imageSize.width" [style.width.%]="imageSize.width"
[style.height.%]="imageSize.height" [style.height.%]="imageSize.height"
[src]="photoSrc" [src]="photoSrc"
(load)="onImageLoad()" (load)="onImageLoad()"
(error)="onImageError()"/> (error)="onImageError()"/>
<video *ngIf="gridMedia !== null && gridMedia.isVideo() && loadMedia"
[style.width.%]="imageSize.width"
[style.height.%]="imageSize.height"
(loadeddata)="logevent($event)"
(loadedmetadata)="logevent($event)"
(durationchange)="logevent($event)"
(loadstart)="logevent($event)"
autoplay
controls
(error)="onImageError()">
<source [src]="gridMedia.getPhotoPath()" type="video/mp4">
</video>
</div> </div>

View File

@ -1,7 +1,8 @@
import {Component, ElementRef, Input, OnChanges} from '@angular/core'; import {Component, ElementRef, Input, OnChanges} from '@angular/core';
import {GridPhoto} from '../../grid/GridPhoto'; import {GridMedia} from '../../grid/GridMedia';
import {PhotoDTO} from '../../../../../common/entities/PhotoDTO'; import {PhotoDTO} from '../../../../../common/entities/PhotoDTO';
import {FixOrientationPipe} from '../../FixOrientationPipe'; import {FixOrientationPipe} from '../../FixOrientationPipe';
import {MediaDTO} from '../../../../../common/entities/MediaDTO';
@Component({ @Component({
selector: 'app-gallery-lightbox-photo', selector: 'app-gallery-lightbox-photo',
@ -10,8 +11,8 @@ import {FixOrientationPipe} from '../../FixOrientationPipe';
}) })
export class GalleryLightboxPhotoComponent implements OnChanges { export class GalleryLightboxPhotoComponent implements OnChanges {
@Input() gridPhoto: GridPhoto; @Input() gridMedia: GridMedia;
@Input() loadImage = false; @Input() loadMedia = false;
@Input() windowAspect = 1; @Input() windowAspect = 1;
prevGirdPhoto = null; prevGirdPhoto = null;
@ -30,28 +31,33 @@ export class GalleryLightboxPhotoComponent implements OnChanges {
this.imageLoaded = false; this.imageLoaded = false;
this.imageLoadFinished = false; this.imageLoadFinished = false;
this.setImageSize(); this.setImageSize();
if (this.prevGirdPhoto !== this.gridPhoto) { if (this.prevGirdPhoto !== this.gridMedia) {
this.prevGirdPhoto = this.gridPhoto; this.prevGirdPhoto = this.gridMedia;
this.thumbnailSrc = null; this.thumbnailSrc = null;
this.photoSrc = null; this.photoSrc = null;
} }
if (this.thumbnailSrc == null && this.gridPhoto && this.ThumbnailUrl !== null) { if (this.thumbnailSrc == null && this.gridMedia && this.ThumbnailUrl !== null) {
FixOrientationPipe.transform(this.ThumbnailUrl, this.gridPhoto.photo.metadata.orientation) FixOrientationPipe.transform(this.ThumbnailUrl, this.gridMedia.Orientation)
.then((src) => this.thumbnailSrc = src); .then((src) => this.thumbnailSrc = src);
} }
if (this.photoSrc == null && this.gridPhoto && this.loadImage) { if (this.photoSrc == null && this.gridMedia && this.loadMedia) {
FixOrientationPipe.transform(this.gridPhoto.getPhotoPath(), this.gridPhoto.photo.metadata.orientation) FixOrientationPipe.transform(this.gridMedia.getPhotoPath(), this.gridMedia.Orientation)
.then((src) => this.thumbnailSrc = src); .then((src) => this.photoSrc = src);
} }
} }
onImageError() { onImageError() {
// TODO:handle error // TODO:handle error
this.imageLoadFinished = true; this.imageLoadFinished = true;
console.error('Error: cannot load image for lightbox url: ' + this.gridPhoto.getPhotoPath()); console.error('Error: cannot load image for lightbox url: ' + this.gridMedia.getPhotoPath());
} }
logevent(ev) {
console.log(ev);
this.imageLoadFinished = true;
this.imageLoaded = true;
}
onImageLoad() { onImageLoad() {
this.imageLoadFinished = true; this.imageLoadFinished = true;
@ -59,34 +65,34 @@ export class GalleryLightboxPhotoComponent implements OnChanges {
} }
private get ThumbnailUrl(): string { private get ThumbnailUrl(): string {
if (this.gridPhoto.isThumbnailAvailable() === true) { if (this.gridMedia.isThumbnailAvailable() === true) {
return this.gridPhoto.getThumbnailPath(); return this.gridMedia.getThumbnailPath();
} }
if (this.gridPhoto.isReplacementThumbnailAvailable() === true) { if (this.gridMedia.isReplacementThumbnailAvailable() === true) {
return this.gridPhoto.getReplacementThumbnailPath(); return this.gridMedia.getReplacementThumbnailPath();
} }
return null; return null;
} }
public get PhotoSrc(): string { public get PhotoSrc(): string {
return this.gridPhoto.getPhotoPath(); return this.gridMedia.getPhotoPath();
} }
public showThumbnail(): boolean { public showThumbnail(): boolean {
return this.gridPhoto && return this.gridMedia &&
!this.imageLoaded && !this.imageLoaded &&
this.thumbnailSrc !== null && this.thumbnailSrc !== null &&
(this.gridPhoto.isThumbnailAvailable() || this.gridPhoto.isReplacementThumbnailAvailable()); (this.gridMedia.isThumbnailAvailable() || this.gridMedia.isReplacementThumbnailAvailable());
} }
private setImageSize() { private setImageSize() {
if (!this.gridPhoto) { if (!this.gridMedia) {
return; return;
} }
const photoAspect = PhotoDTO.calcRotatedAspectRatio(this.gridPhoto.photo); const photoAspect = MediaDTO.calcRotatedAspectRatio(this.gridMedia.media);
if (photoAspect < this.windowAspect) { if (photoAspect < this.windowAspect) {
this.imageSize.height = '100'; this.imageSize.height = '100';

View File

@ -4,10 +4,11 @@ import {Dimension} from '../../../model/IRenderable';
import {FullScreenService} from '../../fullscreen.service'; import {FullScreenService} from '../../fullscreen.service';
import {AgmMap, LatLngBounds, MapsAPILoader} from '@agm/core'; import {AgmMap, LatLngBounds, MapsAPILoader} from '@agm/core';
import {IconThumbnail, Thumbnail, ThumbnailManagerService} from '../../thumnailManager.service'; import {IconThumbnail, Thumbnail, ThumbnailManagerService} from '../../thumnailManager.service';
import {IconPhoto} from '../../IconPhoto'; import {MediaIcon} from '../../MediaIcon';
import {Photo} from '../../Photo'; import {Media} from '../../Media';
import {PageHelper} from '../../../model/page.helper'; import {PageHelper} from '../../../model/page.helper';
import {OrientationTypes} from 'ts-exif-parser'; import {OrientationTypes} from 'ts-exif-parser';
import {MediaDTO} from '../../../../../common/entities/MediaDTO';
@Component({ @Component({
@ -108,13 +109,13 @@ export class GalleryMapLightboxComponent implements OnChanges, AfterViewInit {
}).map(p => { }).map(p => {
let width = 500; let width = 500;
let height = 500; let height = 500;
const rotatedSize = PhotoDTO.getRotatedSize(p); const rotatedSize = MediaDTO.getRotatedSize(p);
if (rotatedSize.width > rotatedSize.height) { if (rotatedSize.width > rotatedSize.height) {
height = width * (rotatedSize.height / rotatedSize.width); height = width * (rotatedSize.height / rotatedSize.width);
} else { } else {
width = height * (rotatedSize.width / rotatedSize.height); width = height * (rotatedSize.width / rotatedSize.height);
} }
const iconTh = this.thumbnailService.getIcon(new IconPhoto(p)); const iconTh = this.thumbnailService.getIcon(new MediaIcon(p));
iconTh.Visible = true; iconTh.Visible = true;
const obj: MapPhoto = { const obj: MapPhoto = {
latitude: p.metadata.positionData.GPSData.latitude, latitude: p.metadata.positionData.GPSData.latitude,
@ -124,7 +125,7 @@ export class GalleryMapLightboxComponent implements OnChanges, AfterViewInit {
preview: { preview: {
width: width, width: width,
height: height, height: height,
thumbnail: this.thumbnailService.getLazyThumbnail(new Photo(p, width, height)) thumbnail: this.thumbnailService.getLazyThumbnail(new Media(p, width, height))
} }
}; };

View File

@ -8,10 +8,10 @@
</ol> </ol>
<div class="right-side"> <div class="right-side">
<div class="photos-count" *ngIf="directory.photos.length > 0 && config.Client.Other.NavBar.showItemCount"> <div class="photos-count" *ngIf="directory.media.length > 0 && config.Client.Other.NavBar.showItemCount">
{{directory.photos.length}} <span i18n>items</span> {{directory.media.length}} <span i18n>items</span>
</div> </div>
<div class="divider" *ngIf="directory.photos.length > 0 && config.Client.Other.NavBar.showItemCount">&nbsp;</div> <div class="divider" *ngIf="directory.media.length > 0 && config.Client.Other.NavBar.showItemCount">&nbsp;</div>
<div class="btn-group" dropdown placement="bottom right"> <div class="btn-group" dropdown placement="bottom right">
<button id="button-alignment" dropdownToggle type="button" <button id="button-alignment" dropdownToggle type="button"
class="btn btn-default dropdown-toggle" aria-controls="dropdown-alignment" class="btn btn-default dropdown-toggle" aria-controls="dropdown-alignment"

View File

@ -1,9 +1,10 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {GalleryCacheService} from './cache.gallery.service'; import {GalleryCacheService} from './cache.gallery.service';
import {Photo} from './Photo'; import {Media} from './Media';
import {IconPhoto} from './IconPhoto'; import {MediaIcon} from './MediaIcon';
import {PhotoDTO} from '../../../common/entities/PhotoDTO'; import {PhotoDTO} from '../../../common/entities/PhotoDTO';
import {Config} from '../../../common/config/public/Config'; import {Config} from '../../../common/config/public/Config';
import {MediaDTO} from '../../../common/entities/MediaDTO';
export enum ThumbnailLoadingPriority { export enum ThumbnailLoadingPriority {
extraHigh = 4, high = 3, medium = 2, low = 1 extraHigh = 4, high = 3, medium = 2, low = 1
@ -35,7 +36,7 @@ export class ThumbnailLoaderService {
const curImg = new Image(); const curImg = new Image();
curImg.onload = () => { curImg.onload = () => {
task.onLoaded(); task.onLoaded();
this.galleryCacheService.photoUpdated(task.photo); this.galleryCacheService.mediaUpdated(task.media);
task.taskEntities.forEach((te: ThumbnailTaskEntity) => te.listener.onLoad()); task.taskEntities.forEach((te: ThumbnailTaskEntity) => te.listener.onLoad());
this.taskReady(task); this.taskReady(task);
@ -73,7 +74,7 @@ export class ThumbnailLoaderService {
} }
loadIcon(photo: IconPhoto, priority: ThumbnailLoadingPriority, listener: ThumbnailLoadingListener): ThumbnailTaskEntity { loadIcon(photo: MediaIcon, priority: ThumbnailLoadingPriority, listener: ThumbnailLoadingListener): ThumbnailTaskEntity {
let thTask: ThumbnailTask = null; let thTask: ThumbnailTask = null;
// is image already qued? // is image already qued?
for (let i = 0; i < this.que.length; i++) { for (let i = 0; i < this.que.length; i++) {
@ -84,7 +85,7 @@ export class ThumbnailLoaderService {
} }
if (thTask == null) { if (thTask == null) {
thTask = { thTask = {
photo: photo.photo, media: photo.media,
inProgress: false, inProgress: false,
taskEntities: [], taskEntities: [],
onLoaded: () => { onLoaded: () => {
@ -106,25 +107,25 @@ export class ThumbnailLoaderService {
return thumbnailTaskEntity; return thumbnailTaskEntity;
} }
loadImage(photo: Photo, priority: ThumbnailLoadingPriority, listener: ThumbnailLoadingListener): ThumbnailTaskEntity { loadImage(media: Media, priority: ThumbnailLoadingPriority, listener: ThumbnailLoadingListener): ThumbnailTaskEntity {
let thTask: ThumbnailTask = null; let thTask: ThumbnailTask = null;
// is image already qued? // is image already qued?
for (let i = 0; i < this.que.length; i++) { for (let i = 0; i < this.que.length; i++) {
if (this.que[i].path === photo.getThumbnailPath()) { if (this.que[i].path === media.getThumbnailPath()) {
thTask = this.que[i]; thTask = this.que[i];
break; break;
} }
} }
if (thTask == null) { if (thTask == null) {
thTask = { thTask = {
photo: photo.photo, media: media.media,
inProgress: false, inProgress: false,
taskEntities: [], taskEntities: [],
onLoaded: () => { onLoaded: () => {
photo.thumbnailLoaded(); media.thumbnailLoaded();
}, },
path: photo.getThumbnailPath() path: media.getThumbnailPath()
}; };
this.que.push(thTask); this.que.push(thTask);
} }
@ -193,7 +194,7 @@ export interface ThumbnailTaskEntity {
} }
interface ThumbnailTask { interface ThumbnailTask {
photo: PhotoDTO; media: MediaDTO;
inProgress: boolean; inProgress: boolean;
taskEntities: Array<ThumbnailTaskEntity>; taskEntities: Array<ThumbnailTaskEntity>;
path: string; path: string;

View File

@ -1,7 +1,7 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {ThumbnailLoaderService, ThumbnailLoadingListener, ThumbnailLoadingPriority, ThumbnailTaskEntity} from './thumnailLoader.service'; import {ThumbnailLoaderService, ThumbnailLoadingListener, ThumbnailLoadingPriority, ThumbnailTaskEntity} from './thumnailLoader.service';
import {Photo} from './Photo'; import {Media} from './Media';
import {IconPhoto} from './IconPhoto'; import {MediaIcon} from './MediaIcon';
@Injectable() @Injectable()
@ -11,16 +11,16 @@ export class ThumbnailManagerService {
constructor(private thumbnailLoader: ThumbnailLoaderService) { constructor(private thumbnailLoader: ThumbnailLoaderService) {
} }
public getThumbnail(photo: Photo): Thumbnail { public getThumbnail(photo: Media): Thumbnail {
return new Thumbnail(photo, this.thumbnailLoader); return new Thumbnail(photo, this.thumbnailLoader);
} }
public getLazyThumbnail(photo: Photo): Thumbnail { public getLazyThumbnail(photo: Media): Thumbnail {
return new Thumbnail(photo, this.thumbnailLoader, false); return new Thumbnail(photo, this.thumbnailLoader, false);
} }
public getIcon(photo: IconPhoto) { public getIcon(photo: MediaIcon) {
return new IconThumbnail(photo, this.thumbnailLoader); return new IconThumbnail(photo, this.thumbnailLoader);
} }
} }
@ -73,19 +73,19 @@ export abstract class ThumbnailBase {
export class IconThumbnail extends ThumbnailBase { export class IconThumbnail extends ThumbnailBase {
constructor(private photo: IconPhoto, thumbnailService: ThumbnailLoaderService) { constructor(private media: MediaIcon, thumbnailService: ThumbnailLoaderService) {
super(thumbnailService); super(thumbnailService);
this.src = ''; this.src = '';
this.error = false; this.error = false;
if (this.photo.isIconAvailable()) { if (this.media.isIconAvailable()) {
this.src = this.photo.getIconPath(); this.src = this.media.getIconPath();
this.available = true; this.available = true;
if (this.onLoad) { if (this.onLoad) {
this.onLoad(); this.onLoad();
} }
} }
if (!this.photo.isIconAvailable()) { if (!this.media.isIconAvailable()) {
setTimeout(() => { setTimeout(() => {
const listener: ThumbnailLoadingListener = { const listener: ThumbnailLoadingListener = {
@ -93,7 +93,7 @@ export class IconThumbnail extends ThumbnailBase {
this.loading = true; this.loading = true;
}, },
onLoad: () => {// onLoaded onLoad: () => {// onLoaded
this.src = this.photo.getIconPath(); this.src = this.media.getIconPath();
if (this.onLoad) { if (this.onLoad) {
this.onLoad(); this.onLoad();
} }
@ -107,7 +107,7 @@ export class IconThumbnail extends ThumbnailBase {
this.error = true; this.error = true;
} }
}; };
this.thumbnailTask = this.thumbnailService.loadIcon(this.photo, ThumbnailLoadingPriority.high, listener); this.thumbnailTask = this.thumbnailService.loadIcon(this.media, ThumbnailLoadingPriority.high, listener);
}, 0); }, 0);
@ -132,16 +132,16 @@ export class IconThumbnail extends ThumbnailBase {
export class Thumbnail extends ThumbnailBase { export class Thumbnail extends ThumbnailBase {
constructor(private photo: Photo, thumbnailService: ThumbnailLoaderService, autoLoad: boolean = true) { constructor(private media: Media, thumbnailService: ThumbnailLoaderService, autoLoad: boolean = true) {
super(thumbnailService); super(thumbnailService);
if (this.photo.isThumbnailAvailable()) { if (this.media.isThumbnailAvailable()) {
this.src = this.photo.getThumbnailPath(); this.src = this.media.getThumbnailPath();
this.available = true; this.available = true;
if (this.onLoad) { if (this.onLoad) {
this.onLoad(); this.onLoad();
} }
} else if (this.photo.isReplacementThumbnailAvailable()) { } else if (this.media.isReplacementThumbnailAvailable()) {
this.src = this.photo.getReplacementThumbnailPath(); this.src = this.media.getReplacementThumbnailPath();
this.available = true; this.available = true;
} }
if (autoLoad) { if (autoLoad) {
@ -154,13 +154,13 @@ export class Thumbnail extends ThumbnailBase {
return; return;
} }
if (value === true) { if (value === true) {
if (this.photo.isReplacementThumbnailAvailable()) { if (this.media.isReplacementThumbnailAvailable()) {
this.thumbnailTask.priority = ThumbnailLoadingPriority.medium; this.thumbnailTask.priority = ThumbnailLoadingPriority.medium;
} else { } else {
this.thumbnailTask.priority = ThumbnailLoadingPriority.extraHigh; this.thumbnailTask.priority = ThumbnailLoadingPriority.extraHigh;
} }
} else { } else {
if (this.photo.isReplacementThumbnailAvailable()) { if (this.media.isReplacementThumbnailAvailable()) {
this.thumbnailTask.priority = ThumbnailLoadingPriority.low; this.thumbnailTask.priority = ThumbnailLoadingPriority.low;
} else { } else {
this.thumbnailTask.priority = ThumbnailLoadingPriority.medium; this.thumbnailTask.priority = ThumbnailLoadingPriority.medium;
@ -173,13 +173,13 @@ export class Thumbnail extends ThumbnailBase {
return; return;
} }
if (visible === true) { if (visible === true) {
if (this.photo.isReplacementThumbnailAvailable()) { if (this.media.isReplacementThumbnailAvailable()) {
this.thumbnailTask.priority = ThumbnailLoadingPriority.medium; this.thumbnailTask.priority = ThumbnailLoadingPriority.medium;
} else { } else {
this.thumbnailTask.priority = ThumbnailLoadingPriority.high; this.thumbnailTask.priority = ThumbnailLoadingPriority.high;
} }
} else { } else {
if (this.photo.isReplacementThumbnailAvailable()) { if (this.media.isReplacementThumbnailAvailable()) {
this.thumbnailTask.priority = ThumbnailLoadingPriority.low; this.thumbnailTask.priority = ThumbnailLoadingPriority.low;
} else { } else {
this.thumbnailTask.priority = ThumbnailLoadingPriority.medium; this.thumbnailTask.priority = ThumbnailLoadingPriority.medium;
@ -188,14 +188,14 @@ export class Thumbnail extends ThumbnailBase {
} }
public load() { public load() {
if (!this.photo.isThumbnailAvailable() && this.thumbnailTask == null) { if (!this.media.isThumbnailAvailable() && this.thumbnailTask == null) {
// setTimeout(() => { // setTimeout(() => {
const listener: ThumbnailLoadingListener = { const listener: ThumbnailLoadingListener = {
onStartedLoading: () => { // onLoadStarted onStartedLoading: () => { // onLoadStarted
this.loading = true; this.loading = true;
}, },
onLoad: () => {// onLoaded onLoad: () => {// onLoaded
this.src = this.photo.getThumbnailPath(); this.src = this.media.getThumbnailPath();
if (this.onLoad) { if (this.onLoad) {
this.onLoad(); this.onLoad();
} }
@ -209,10 +209,10 @@ export class Thumbnail extends ThumbnailBase {
this.error = true; this.error = true;
} }
}; };
if (this.photo.isReplacementThumbnailAvailable()) { if (this.media.isReplacementThumbnailAvailable()) {
this.thumbnailTask = this.thumbnailService.loadImage(this.photo, ThumbnailLoadingPriority.medium, listener); this.thumbnailTask = this.thumbnailService.loadImage(this.media, ThumbnailLoadingPriority.medium, listener);
} else { } else {
this.thumbnailTask = this.thumbnailService.loadImage(this.photo, ThumbnailLoadingPriority.high, listener); this.thumbnailTask = this.thumbnailService.loadImage(this.media, ThumbnailLoadingPriority.high, listener);
} }
// }, 0); // }, 0);
} }

View File

@ -1,6 +1,7 @@
import {Injectable} from '@angular/core'; import {Injectable} from '@angular/core';
import {ShareService} from '../gallery/share.service'; import {ShareService} from '../gallery/share.service';
import {PhotoDTO} from '../../../common/entities/PhotoDTO'; import {PhotoDTO} from '../../../common/entities/PhotoDTO';
import {MediaDTO} from '../../../common/entities/MediaDTO';
@Injectable() @Injectable()
export class QueryService { export class QueryService {
@ -10,10 +11,10 @@ export class QueryService {
constructor(private shareService: ShareService) { constructor(private shareService: ShareService) {
} }
getParams(photo?: PhotoDTO): { [key: string]: string } { getParams(media?: MediaDTO): { [key: string]: string } {
const query = {}; const query = {};
if (photo) { if (media) {
query[QueryService.PHOTO_PARAM] = photo.name; query[QueryService.PHOTO_PARAM] = media.name;
} }
if (this.shareService.isSharing()) { if (this.shareService.isSharing()) {
query['sk'] = this.shareService.getSharingKey(); query['sk'] = this.shareService.getSharingKey();

View File

@ -21,7 +21,7 @@ export class RandomPhotoSettingsComponent extends SettingsComponent<ClientConfig
_settingsService: RandomPhotoSettingsService, _settingsService: RandomPhotoSettingsService,
notification: NotificationService, notification: NotificationService,
i18n: I18n) { i18n: I18n) {
super(i18n('Random Photo'), _authService, _navigation, _settingsService, notification, i18n, s => s.Client.RandomPhoto); super(i18n('Random Media'), _authService, _navigation, _settingsService, notification, i18n, s => s.Client.RandomPhoto);
} }

View File

@ -34,6 +34,7 @@
"cookie-session": "2.0.0-beta.3", "cookie-session": "2.0.0-beta.3",
"ejs": "2.6.1", "ejs": "2.6.1",
"express": "4.16.4", "express": "4.16.4",
"fluent-ffmpeg": "^2.1.2",
"jimp": "0.5.6", "jimp": "0.5.6",
"locale": "0.1.0", "locale": "0.1.0",
"reflect-metadata": "0.1.12", "reflect-metadata": "0.1.12",
@ -65,6 +66,7 @@
"@types/chai": "4.1.7", "@types/chai": "4.1.7",
"@types/cookie-session": "2.0.36", "@types/cookie-session": "2.0.36",
"@types/express": "4.16.0", "@types/express": "4.16.0",
"@types/fluent-ffmpeg": "^2.1.8",
"@types/gm": "1.18.1", "@types/gm": "1.18.1",
"@types/jasmine": "2.8.9", "@types/jasmine": "2.8.9",
"@types/node": "10.12.2", "@types/node": "10.12.2",

28
sandbox.ts Normal file
View File

@ -0,0 +1,28 @@
import * as FfmpegCommand from 'fluent-ffmpeg';
const run = async () => {
const command = FfmpegCommand('demo/images/fulbright_mediumbr.mp4');
// command.setFfmpegPath('ffmpeg/ffmpeg.exe');
// command.setFfprobePath('ffmpeg/ffprobe.exe');
// command.setFlvtoolPath('ffmpeg/ffplay.exe');
FfmpegCommand('demo/images/fulbright_mediumbr.mp4').ffprobe((err,data) => {
console.log(data);
});
command // setup event handlers
.on('filenames', function (filenames) {
console.log('screenshots are ' + filenames.join(', '));
})
.on('end', function () {
console.log('screenshots were saved');
})
.on('error', function (err) {
console.log('an error happened: ' + err.message);
})
.outputOptions(['-qscale:v 4'])
// take 2 screenshots at predefined timemarks and size
.takeScreenshots({timemarks: ['10%'], size: '450x?', filename: 'thumbnail2-at-%s-seconds.jpg'});
};
run();

View File

@ -61,7 +61,7 @@ describe('Typeorm integration', () => {
d.lastModified = Date.now(); d.lastModified = Date.now();
d.lastScanned = null; d.lastScanned = null;
d.parent = null; d.parent = null;
d.photos = []; d.media = [];
d.directories = []; d.directories = [];
return d; return d;
}; };
@ -98,7 +98,7 @@ describe('Typeorm integration', () => {
const d = new PhotoEntity(); const d = new PhotoEntity();
d.name = 'test photo.jpg'; d.name = 'test media.jpg';
d.directory = null; d.directory = null;
d.metadata = m; d.metadata = m;
return d; return d;
@ -142,7 +142,7 @@ describe('Typeorm integration', () => {
expect((await dr.find()).length).to.equal(1); expect((await dr.find()).length).to.equal(1);
}); });
it('should add a photo', async () => { it('should add a media', async () => {
const conn = await SQLConnection.getConnection(); const conn = await SQLConnection.getConnection();
const pr = conn.getRepository(PhotoEntity); const pr = conn.getRepository(PhotoEntity);
const dir = await conn.getRepository(DirectoryEntity).save(getDir()); const dir = await conn.getRepository(DirectoryEntity).save(getDir());
@ -152,7 +152,7 @@ describe('Typeorm integration', () => {
expect((await pr.find()).length).to.equal(1); expect((await pr.find()).length).to.equal(1);
}); });
it('should find a photo', async () => { it('should find a media', async () => {
const conn = await SQLConnection.getConnection(); const conn = await SQLConnection.getConnection();
const pr = conn.getRepository(PhotoEntity); const pr = conn.getRepository(PhotoEntity);
const dir = await conn.getRepository(DirectoryEntity).save(getDir()); const dir = await conn.getRepository(DirectoryEntity).save(getDir());
@ -161,10 +161,10 @@ describe('Typeorm integration', () => {
await pr.save(photo); await pr.save(photo);
let photos = await pr let photos = await pr
.createQueryBuilder('photo') .createQueryBuilder('media')
.orderBy('photo.metadata.creationDate', 'ASC') .orderBy('media.metadata.creationDate', 'ASC')
.where('photo.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + photo.metadata.positionData.city + '%'}) .where('media.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + photo.metadata.positionData.city + '%'})
.innerJoinAndSelect('photo.directory', 'directory') .innerJoinAndSelect('media.directory', 'directory')
.limit(10) .limit(10)
.getMany(); .getMany();
@ -173,7 +173,7 @@ describe('Typeorm integration', () => {
}); });
it('should not find a photo', async () => { it('should not find a media', async () => {
const conn = await SQLConnection.getConnection(); const conn = await SQLConnection.getConnection();
const pr = conn.getRepository(PhotoEntity); const pr = conn.getRepository(PhotoEntity);
const dir = await conn.getRepository(DirectoryEntity).save(getDir()); const dir = await conn.getRepository(DirectoryEntity).save(getDir());
@ -183,17 +183,17 @@ describe('Typeorm integration', () => {
photo.metadata.positionData = null; photo.metadata.positionData = null;
await pr.save(photo); await pr.save(photo);
let photos = await pr let photos = await pr
.createQueryBuilder('photo') .createQueryBuilder('media')
.orderBy('photo.metadata.creationDate', 'ASC') .orderBy('media.metadata.creationDate', 'ASC')
.where('photo.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + city + '%'}) .where('media.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + city + '%'})
.innerJoinAndSelect('photo.directory', 'directory') .innerJoinAndSelect('media.directory', 'directory')
.limit(10) .limit(10)
.getMany(); .getMany();
expect(photos.length).to.equal(0); expect(photos.length).to.equal(0);
}); });
it('should open and close connection twice with photo added ', async () => { it('should open and close connection twice with media added ', async () => {
let conn = await SQLConnection.getConnection(); let conn = await SQLConnection.getConnection();
const dir = await conn.getRepository(DirectoryEntity).save(getDir()); const dir = await conn.getRepository(DirectoryEntity).save(getDir());
let dir2 = getDir(); let dir2 = getDir();

View File

@ -60,7 +60,7 @@ describe('SearchManager', () => {
const d = new PhotoEntity(); const d = new PhotoEntity();
d.name = 'test photo.jpg'; d.name = 'test media.jpg';
d.directory = dir; d.directory = dir;
d.metadata = m; d.metadata = m;
return d; return d;
@ -159,7 +159,7 @@ describe('SearchManager', () => {
searchText: 'sw', searchText: 'sw',
searchType: null, searchType: null,
directories: [], directories: [],
photos: [p, p2], media: [p, p2],
resultOverflow: false resultOverflow: false
}); });
@ -167,7 +167,7 @@ describe('SearchManager', () => {
searchText: 'Tatooine', searchText: 'Tatooine',
searchType: SearchTypes.position, searchType: SearchTypes.position,
directories: [], directories: [],
photos: [p], media: [p],
resultOverflow: false resultOverflow: false
}); });
@ -175,7 +175,7 @@ describe('SearchManager', () => {
searchText: 'ortm', searchText: 'ortm',
searchType: SearchTypes.keyword, searchType: SearchTypes.keyword,
directories: [], directories: [],
photos: [p2], media: [p2],
resultOverflow: false resultOverflow: false
}); });
@ -183,7 +183,7 @@ describe('SearchManager', () => {
searchText: 'ortm', searchText: 'ortm',
searchType: SearchTypes.keyword, searchType: SearchTypes.keyword,
directories: [], directories: [],
photos: [p2], media: [p2],
resultOverflow: false resultOverflow: false
}); });
@ -191,7 +191,7 @@ describe('SearchManager', () => {
searchText: 'wa', searchText: 'wa',
searchType: SearchTypes.keyword, searchType: SearchTypes.keyword,
directories: [dir], directories: [dir],
photos: [p, p2], media: [p, p2],
resultOverflow: false resultOverflow: false
}); });
}); });

View File

@ -3,6 +3,7 @@ import {DiskMangerWorker} from '../../../../../backend/model/threading/DiskMange
import * as path from 'path'; import * as path from 'path';
import {Config} from '../../../../../common/config/private/Config'; import {Config} from '../../../../../common/config/private/Config';
import {ProjectPath} from '../../../../../backend/ProjectPath'; import {ProjectPath} from '../../../../../backend/ProjectPath';
import {PhotoDTO} from '../../../../../common/entities/PhotoDTO';
describe('DiskMangerWorker', () => { describe('DiskMangerWorker', () => {
@ -10,12 +11,12 @@ describe('DiskMangerWorker', () => {
Config.Server.imagesFolder = path.join(__dirname, '/../../assets'); Config.Server.imagesFolder = path.join(__dirname, '/../../assets');
ProjectPath.ImageFolder = path.join(__dirname, '/../../assets'); ProjectPath.ImageFolder = path.join(__dirname, '/../../assets');
const dir = await DiskMangerWorker.scanDirectory('/'); const dir = await DiskMangerWorker.scanDirectory('/');
expect(dir.photos.length).to.be.equals(1); expect(dir.media.length).to.be.equals(1);
expect(dir.photos[0].name).to.be.equals('test image öüóőúéáű-.,.jpg'); expect(dir.media[0].name).to.be.equals('test image öüóőúéáű-.,.jpg');
expect(dir.photos[0].metadata.keywords).to.deep.equals(['Berkley', 'USA', 'űáéúőóüö ŰÁÉÚŐÓÜÖ']); expect(dir.media[0].metadata.keywords).to.deep.equals(['Berkley', 'USA', 'űáéúőóüö ŰÁÉÚŐÓÜÖ']);
expect(dir.photos[0].metadata.fileSize).to.deep.equals(62392); expect(dir.media[0].metadata.fileSize).to.deep.equals(62392);
expect(dir.photos[0].metadata.size).to.deep.equals({width: 140, height: 93}); expect(dir.media[0].metadata.size).to.deep.equals({width: 140, height: 93});
expect(dir.photos[0].metadata.cameraData).to.deep.equals({ expect((<PhotoDTO>dir.media[0]).metadata.cameraData).to.deep.equals({
ISO: 3200, ISO: 3200,
model: 'óüöúőűáé ÓÜÖÚŐŰÁÉ', model: 'óüöúőűáé ÓÜÖÚŐŰÁÉ',
make: 'Canon', make: 'Canon',
@ -25,7 +26,7 @@ describe('DiskMangerWorker', () => {
lens: 'EF-S15-85mm f/3.5-5.6 IS USM' lens: 'EF-S15-85mm f/3.5-5.6 IS USM'
}); });
expect(dir.photos[0].metadata.positionData).to.deep.equals({ expect(dir.media[0].metadata.positionData).to.deep.equals({
GPSData: { GPSData: {
latitude: 37.871093333333334, latitude: 37.871093333333334,
longitude: -122.25678, longitude: -122.25678,
@ -36,7 +37,7 @@ describe('DiskMangerWorker', () => {
city: 'óüöúőűáé ÓÜÖÚŐŰÁ' city: 'óüöúőűáé ÓÜÖÚŐŰÁ'
}); });
expect(dir.photos[0].metadata.creationDate).to.be.equals(1434018566000); expect(dir.media[0].metadata.creationDate).to.be.equals(1434018566000);
}); });