From f13f333d4961d8b2078165f7057f3bb337a5c860 Mon Sep 17 00:00:00 2001 From: "Patrik J. Braun" Date: Sun, 4 Nov 2018 19:28:32 +0100 Subject: [PATCH] implementing basic video support --- backend/middlewares/GalleryMWs.ts | 24 ++--- .../thumbnail/ThumbnailGeneratorMWs.ts | 80 +++++++++-------- backend/model/ConfigDiagnostics.ts | 4 +- backend/model/DiskManger.ts | 2 +- backend/model/memory/GalleryManager.ts | 2 +- backend/model/sql/GalleryManager.ts | 82 ++++++++--------- backend/model/sql/SearchManager.ts | 68 +++++++------- backend/model/sql/enitites/DirectoryEntity.ts | 6 +- backend/model/sql/enitites/PhotoEntity.ts | 7 +- backend/model/threading/DiskMangerWorker.ts | 80 +++++++++++++++-- backend/model/threading/ThumbnailWorker.ts | 88 ++++++++++++++++--- backend/routes/GalleryRouter.ts | 40 +++++++-- common/entities/DirectoryDTO.ts | 6 +- common/entities/MediaDTO.ts | 78 ++++++++++++++++ common/entities/PhotoDTO.ts | 60 +------------ common/entities/SearchResultDTO.ts | 2 +- common/entities/VideoDTO.ts | 9 ++ frontend/app/gallery/FixOrientationPipe.ts | 2 +- frontend/app/gallery/IconPhoto.ts | 50 ----------- frontend/app/gallery/{Photo.ts => Media.ts} | 29 +++--- frontend/app/gallery/MediaIcon.ts | 51 +++++++++++ frontend/app/gallery/cache.gallery.service.ts | 17 ++-- .../directory/directory.gallery.component.css | 2 +- .../directory/directory.gallery.component.ts | 13 +-- frontend/app/gallery/gallery.component.html | 8 +- frontend/app/gallery/gallery.component.ts | 12 +-- frontend/app/gallery/gallery.service.ts | 2 +- frontend/app/gallery/grid/GridMedia.ts | 26 ++++++ frontend/app/gallery/grid/GridPhoto.ts | 12 --- frontend/app/gallery/grid/GridRowBuilder.ts | 7 +- .../gallery/grid/grid.gallery.component.html | 2 +- .../gallery/grid/grid.gallery.component.ts | 13 +-- .../photo/photo.grid.gallery.component.html | 8 +- .../photo/photo.grid.gallery.component.ts | 10 +-- ...info-panel.lightbox.gallery.component.html | 40 ++++----- .../info-panel.lightbox.gallery.component.ts | 31 ++++--- .../lightbox/lightbox.gallery.component.html | 8 +- .../lightbox/lightbox.gallery.component.ts | 33 +++---- .../photo.lightbox.gallery.component.css | 3 +- .../photo.lightbox.gallery.component.html | 15 +++- .../photo/photo.lightbox.gallery.component.ts | 46 +++++----- .../lightbox.map.gallery.component.ts | 11 +-- .../navigator.gallery.component.html | 6 +- .../app/gallery/thumnailLoader.service.ts | 23 ++--- .../app/gallery/thumnailManager.service.ts | 50 +++++------ frontend/app/model/query.service.ts | 7 +- .../random-photo.settings.component.ts | 2 +- package.json | 2 + sandbox.ts | 28 ++++++ test/backend/integration/model/sql/typeorm.ts | 28 +++--- test/backend/unit/model/sql/SearchManager.ts | 12 +-- .../model/threading/DiskMangerWorker.spec.ts | 17 ++-- 52 files changed, 764 insertions(+), 500 deletions(-) create mode 100644 common/entities/MediaDTO.ts create mode 100644 common/entities/VideoDTO.ts delete mode 100644 frontend/app/gallery/IconPhoto.ts rename frontend/app/gallery/{Photo.ts => Media.ts} (59%) create mode 100644 frontend/app/gallery/MediaIcon.ts create mode 100644 frontend/app/gallery/grid/GridMedia.ts delete mode 100644 frontend/app/gallery/grid/GridPhoto.ts create mode 100644 sandbox.ts diff --git a/backend/middlewares/GalleryMWs.ts b/backend/middlewares/GalleryMWs.ts index 9960d767..f372c963 100644 --- a/backend/middlewares/GalleryMWs.ts +++ b/backend/middlewares/GalleryMWs.ts @@ -59,8 +59,8 @@ export class GalleryMWs { if (cw.notModified === true) { return next(); } - const removeDirs = (dir) => { - dir.photos.forEach((photo: PhotoDTO) => { + const removeDirs = (dir: DirectoryDTO) => { + dir.media.forEach((photo: PhotoDTO) => { photo.directory = null; }); @@ -119,28 +119,28 @@ export class GalleryMWs { 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(); } - public static loadImage(req: Request, res: Response, next: NextFunction) { - if (!(req.params.imagePath)) { + public static loadMedia(req: Request, res: Response, next: NextFunction) { + if (!(req.params.mediaPath)) { 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 - if (fs.existsSync(fullImagePath) === false) { - return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'no such file:' + fullImagePath)); + if (fs.existsSync(fullMediaPath) === false) { + return next(new ErrorDTO(ErrorCodes.GENERAL_ERROR, 'no such file:' + fullMediaPath)); } - if (fs.statSync(fullImagePath).isDirectory()) { + if (fs.statSync(fullMediaPath).isDirectory()) { return next(); } - req.resultPipe = fullImagePath; + req.resultPipe = fullMediaPath; return next(); } @@ -161,7 +161,7 @@ export class GalleryMWs { try { 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); return next(); } catch (err) { @@ -181,7 +181,7 @@ export class GalleryMWs { try { 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); return next(); } catch (err) { diff --git a/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts b/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts index 1b6ae226..2789ae81 100644 --- a/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts +++ b/backend/middlewares/thumbnail/ThumbnailGeneratorMWs.ts @@ -12,8 +12,9 @@ import {PhotoDTO} from '../../../common/entities/PhotoDTO'; import {Config} from '../../../common/config/private/Config'; import {ThumbnailProcessingLib} from '../../../common/config/private/IPrivateConfig'; 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 {MediaDTO} from '../../../common/entities/MediaDTO'; export class ThumbnailGeneratorMWs { @@ -60,7 +61,7 @@ export class ThumbnailGeneratorMWs { ThumbnailGeneratorMWs.addThInfoTODir(cw.directory); } if (cw.searchResult) { - ThumbnailGeneratorMWs.addThInfoToPhotos(cw.searchResult.photos); + ThumbnailGeneratorMWs.addThInfoToPhotos(cw.searchResult.media); } @@ -68,46 +69,47 @@ export class ThumbnailGeneratorMWs { } - public static generateThumbnail(req: Request, res: Response, next: NextFunction) { - if (!req.resultPipe) { - return next(); - } + public static generateThumbnailFactory(sourceType: ThumbnailSourceType) { + return (req: Request, res: Response, next: NextFunction) => { + if (!req.resultPipe) { + return next(); + } - // load parameters - const imagePath = req.resultPipe; - let size: number = parseInt(req.params.size, 10) || Config.Client.Thumbnail.thumbnailSizes[0]; - - // validate size - if (Config.Client.Thumbnail.thumbnailSizes.indexOf(size) === -1) { - size = Config.Client.Thumbnail.thumbnailSizes[0]; - } - - ThumbnailGeneratorMWs.generateImage(imagePath, size, false, req, res, next); + // load parameters + const mediaPath = req.resultPipe; + let size: number = parseInt(req.params.size, 10) || Config.Client.Thumbnail.thumbnailSizes[0]; + // validate size + if (Config.Client.Thumbnail.thumbnailSizes.indexOf(size) === -1) { + size = Config.Client.Thumbnail.thumbnailSizes[0]; + } + ThumbnailGeneratorMWs.generateImage(mediaPath, size, sourceType, false, req, res, next); + }; } - public static generateIcon(req: Request, res: Response, next: NextFunction) { - if (!req.resultPipe) { - return next(); - } - - // load parameters - const imagePath = req.resultPipe; - const size: number = Config.Client.Thumbnail.iconSize; - ThumbnailGeneratorMWs.generateImage(imagePath, size, true, req, res, next); + public static generateIconFactory(sourceType: ThumbnailSourceType) { + return (req: Request, res: Response, next: NextFunction) => { + if (!req.resultPipe) { + return next(); + } + // load parameters + const mediaPath = req.resultPipe; + const size: number = Config.Client.Thumbnail.iconSize; + ThumbnailGeneratorMWs.generateImage(mediaPath, size, sourceType, true, req, res, next); + }; } private static addThInfoTODir(directory: DirectoryDTO) { - if (typeof directory.photos === 'undefined') { - directory.photos = []; + if (typeof directory.media === 'undefined') { + directory.media = []; } if (typeof directory.directories === 'undefined') { directory.directories = []; } - ThumbnailGeneratorMWs.addThInfoToPhotos(directory.photos); + ThumbnailGeneratorMWs.addThInfoToPhotos(directory.media); for (let i = 0; i < directory.directories.length; i++) { ThumbnailGeneratorMWs.addThInfoTODir(directory.directories[i]); @@ -115,13 +117,13 @@ export class ThumbnailGeneratorMWs { } - private static addThInfoToPhotos(photos: Array) { + private static addThInfoToPhotos(photos: MediaDTO[]) { const thumbnailFolder = ProjectPath.ThumbnailFolder; 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++) { 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 (typeof photos[i].readyThumbnails === 'undefined') { photos[i].readyThumbnails = []; @@ -130,7 +132,7 @@ export class ThumbnailGeneratorMWs { } } const iconPath = path.join(thumbnailFolder, - ThumbnailGeneratorMWs.generateThumbnailName(fullImagePath, Config.Client.Thumbnail.iconSize)); + ThumbnailGeneratorMWs.generateThumbnailName(fullMediaPath, Config.Client.Thumbnail.iconSize)); if (fs.existsSync(iconPath) === 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, + sourceType: ThumbnailSourceType, makeSquare: boolean, req: Request, res: Response, next: NextFunction) { // 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; @@ -160,7 +163,8 @@ export class ThumbnailGeneratorMWs { // run on other thread const input = { - imagePath: imagePath, + type: sourceType, + mediaPath: mediaPath, size: size, thPath: thPath, makeSquare: makeSquare, @@ -170,12 +174,12 @@ export class ThumbnailGeneratorMWs { await this.taskQue.execute(input); return next(); } 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 { - return crypto.createHash('md5').update(imagePath).digest('hex') + '_' + size + '.jpg'; + private static generateThumbnailName(mediaPath: string, size: number): string { + return crypto.createHash('md5').update(mediaPath).digest('hex') + '_' + size + '.jpg'; } } diff --git a/backend/model/ConfigDiagnostics.ts b/backend/model/ConfigDiagnostics.ts index 6fced012..c610be40 100644 --- a/backend/model/ConfigDiagnostics.ts +++ b/backend/model/ConfigDiagnostics.ts @@ -204,9 +204,9 @@ export class ConfigDiagnostics { await ConfigDiagnostics.testRandomPhotoConfig(Config.Client.Sharing, Config); } catch (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()); - 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; } diff --git a/backend/model/DiskManger.ts b/backend/model/DiskManger.ts index f5914630..3a111c8a 100644 --- a/backend/model/DiskManger.ts +++ b/backend/model/DiskManger.ts @@ -28,7 +28,7 @@ export class DiskManager { directory = await DiskMangerWorker.scanDirectory(relativeDirectoryName); } const addDirs = (dir: DirectoryDTO) => { - dir.photos.forEach((ph) => { + dir.media.forEach((ph) => { ph.directory = dir; }); dir.directories.forEach((d) => { diff --git a/backend/model/memory/GalleryManager.ts b/backend/model/memory/GalleryManager.ts index d6e36ac7..122f4bd1 100644 --- a/backend/model/memory/GalleryManager.ts +++ b/backend/model/memory/GalleryManager.ts @@ -25,6 +25,6 @@ export class GalleryManager implements IGalleryManager { } getRandomPhoto(RandomQuery): Promise { - throw new Error('Random photo is not supported without database'); + throw new Error('Random media is not supported without database'); } } diff --git a/backend/model/sql/GalleryManager.ts b/backend/model/sql/GalleryManager.ts index 27a7f542..f7055bf9 100644 --- a/backend/model/sql/GalleryManager.ts +++ b/backend/model/sql/GalleryManager.ts @@ -53,30 +53,30 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { return null; } } - if (dir.photos) { - for (let i = 0; i < dir.photos.length; i++) { - dir.photos[i].directory = dir; - dir.photos[i].readyThumbnails = []; - dir.photos[i].readyIcon = false; + if (dir.media) { + for (let i = 0; i < dir.media.length; i++) { + dir.media[i].directory = dir; + dir.media[i].readyThumbnails = []; + dir.media[i].readyIcon = false; } } if (dir.directories) { for (let i = 0; i < dir.directories.length; i++) { - dir.directories[i].photos = await connection + dir.directories[i].media = await connection .getRepository(PhotoEntity) - .createQueryBuilder('photo') - .where('photo.directory = :dir', { + .createQueryBuilder('media') + .where('media.directory = :dir', { dir: dir.directories[i].id }) - .orderBy('photo.metadata.creationDate', 'ASC') + .orderBy('media.metadata.creationDate', 'ASC') .limit(Config.Server.indexing.folderPreviewSize) .getMany(); dir.directories[i].isPartial = true; - for (let j = 0; j < dir.directories[i].photos.length; j++) { - dir.directories[i].photos[j].directory = dir.directories[i]; - dir.directories[i].photos[j].readyThumbnails = []; - dir.directories[i].photos[j].readyIcon = false; + for (let j = 0; j < dir.directories[i].media.length; j++) { + dir.directories[i].media[j].directory = dir.directories[i]; + dir.directories[i].media[j].readyThumbnails = []; + dir.directories[i].media[j].readyIcon = false; } } } @@ -113,7 +113,7 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { const scannedDirectory = await DiskManager.scanDirectory(relativeDirectoryName); // returning with the result - scannedDirectory.photos.forEach(p => p.readyThumbnails = []); + scannedDirectory.media.forEach(p => p.readyThumbnails = []); resolve(scannedDirectory); await this.saveToDB(scannedDirectory); @@ -169,18 +169,18 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { if (directory != null) { // update existing directory if (!directory.parent || !directory.parent.id) { // set parent if not set yet directory.parent = currentDir; - delete directory.photos; + delete directory.media; await directoryRepository.save(directory); } } else { scannedDirectory.directories[i].parent = currentDir; (scannedDirectory.directories[i]).lastScanned = null; // new child dir, not fully scanned yet const d = await directoryRepository.save(scannedDirectory.directories[i]); - for (let j = 0; j < scannedDirectory.directories[i].photos.length; j++) { - scannedDirectory.directories[i].photos[j].directory = d; + for (let j = 0; j < scannedDirectory.directories[i].media.length; j++) { + 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); - const indexedPhotos = await photosRepository.createQueryBuilder('photo') - .where('photo.directory = :dir', { + const indexedPhotos = await photosRepository.createQueryBuilder('media') + .where('media.directory = :dir', { dir: currentDir.id }).getMany(); const photosToSave = []; - for (let i = 0; i < scannedDirectory.photos.length; i++) { + for (let i = 0; i < scannedDirectory.media.length; i++) { let photo = null; 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]; indexedPhotos.splice(j, 1); break; } } if (photo == null) { - scannedDirectory.photos[i].directory = null; - photo = Utils.clone(scannedDirectory.photos[i]); - scannedDirectory.photos[i].directory = scannedDirectory; + scannedDirectory.media[i].directory = null; + photo = Utils.clone(scannedDirectory.media[i]); + scannedDirectory.media[i].directory = scannedDirectory; photo.directory = currentDir; } - if (photo.metadata.keywords !== scannedDirectory.photos[i].metadata.keywords || - photo.metadata.cameraData !== scannedDirectory.photos[i].metadata.cameraData || - photo.metadata.positionData !== scannedDirectory.photos[i].metadata.positionData || - photo.metadata.size !== scannedDirectory.photos[i].metadata.size) { + if (photo.metadata.keywords !== scannedDirectory.media[i].metadata.keywords || + photo.metadata.cameraData !== (scannedDirectory.media[i]).metadata.cameraData || + photo.metadata.positionData !== scannedDirectory.media[i].metadata.positionData || + photo.metadata.size !== scannedDirectory.media[i].metadata.size) { - photo.metadata.keywords = scannedDirectory.photos[i].metadata.keywords; - photo.metadata.cameraData = scannedDirectory.photos[i].metadata.cameraData; - photo.metadata.positionData = scannedDirectory.photos[i].metadata.positionData; - photo.metadata.size = scannedDirectory.photos[i].metadata.size; + photo.metadata.keywords = scannedDirectory.media[i].metadata.keywords; + photo.metadata.cameraData = (scannedDirectory.media[i]).metadata.cameraData; + photo.metadata.positionData = scannedDirectory.media[i].metadata.positionData; + photo.metadata.size = scannedDirectory.media[i].metadata.size; photosToSave.push(photo); } } @@ -233,8 +233,8 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { const connection = await SQLConnection.getConnection(); const photosRepository = connection.getRepository(PhotoEntity); - const query = photosRepository.createQueryBuilder('photo'); - query.innerJoinAndSelect('photo.directory', 'directory'); + const query = photosRepository.createQueryBuilder('media'); + query.innerJoinAndSelect('media.directory', 'directory'); if (queryFilter.directory) { const directoryName = path.basename(queryFilter.directory); @@ -253,31 +253,31 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { } if (queryFilter.fromDate) { - query.andWhere('photo.metadata.creationDate >= :fromDate', { + query.andWhere('media.metadata.creationDate >= :fromDate', { fromDate: queryFilter.fromDate.getTime() }); } if (queryFilter.toDate) { - query.andWhere('photo.metadata.creationDate <= :toDate', { + query.andWhere('media.metadata.creationDate <= :toDate', { toDate: queryFilter.toDate.getTime() }); } 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 }); } 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 }); } 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) { - query.andWhere('photo.metadata.size.width <= photo.metadata.size.height'); + query.andWhere('media.metadata.size.width <= media.metadata.size.height'); } diff --git a/backend/model/sql/SearchManager.ts b/backend/model/sql/SearchManager.ts index 80ca2dad..436ce488 100644 --- a/backend/model/sql/SearchManager.ts +++ b/backend/model/sql/SearchManager.ts @@ -30,9 +30,9 @@ export class SearchManager implements ISearchManager { (await photoRepository - .createQueryBuilder('photo') - .select('DISTINCT(photo.metadata.keywords)') - .where('photo.metadata.keywords LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .createQueryBuilder('media') + .select('DISTINCT(media.metadata.keywords)') + .where('media.metadata.keywords LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) .limit(5) .getRawMany()) .map(r => >r.metadataKeywords.split(',')) @@ -43,13 +43,13 @@ export class SearchManager implements ISearchManager { (await photoRepository - .createQueryBuilder('photo') - .select('photo.metadata.positionData.country as country,' + - ' photo.metadata.positionData.state as state, photo.metadata.positionData.city as city') - .where('photo.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('photo.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) - .groupBy('photo.metadata.positionData.country, photo.metadata.positionData.state, photo.metadata.positionData.city') + .createQueryBuilder('media') + .select('media.metadata.positionData.country as country,' + + 'mediao.metadata.positionData.state as state, media.metadata.positionData.city as city') + .where('media.metadata.positionData.country LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .orWhere('media.metadata.positionData.state LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .orWhere('media.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .groupBy('media.metadata.positionData.country, media.metadata.positionData.state, media.metadata.positionData.city') .limit(5) .getRawMany()) .filter(pm => !!pm) @@ -60,9 +60,9 @@ export class SearchManager implements ISearchManager { }); result = result.concat(this.encapsulateAutoComplete((await photoRepository - .createQueryBuilder('photo') - .select('DISTINCT(photo.name)') - .where('photo.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .createQueryBuilder('media') + .select('DISTINCT(media.name)') + .where('media.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) .limit(5) .getRawMany()) .map(r => r.name), SearchTypes.image)); @@ -86,15 +86,15 @@ export class SearchManager implements ISearchManager { searchText: text, searchType: searchType, directories: [], - photos: [], + media: [], resultOverflow: false }; const query = connection .getRepository(PhotoEntity) - .createQueryBuilder('photo') - .innerJoinAndSelect('photo.directory', 'directory') - .orderBy('photo.metadata.creationDate', 'ASC'); + .createQueryBuilder('media') + .innerJoinAndSelect('media.directory', 'directory') + .orderBy('media.metadata.creationDate', 'ASC'); if (!searchType || searchType === SearchTypes.directory) { @@ -102,24 +102,24 @@ export class SearchManager implements ISearchManager { } 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) { - query.orWhere('photo.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('photo.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}); + query.orWhere('media.metadata.positionData.country LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .orWhere('media.metadata.positionData.state 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) { - 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) .getMany(); - if (result.photos.length > 2000) { + if (result.media.length > 2000) { result.resultOverflow = true; } @@ -144,20 +144,20 @@ export class SearchManager implements ISearchManager { searchText: text, // searchType:undefined, not adding this directories: [], - photos: [], + media: [], resultOverflow: false }; - result.photos = await connection + result.media = await connection .getRepository(PhotoEntity) - .createQueryBuilder('photo') - .orderBy('photo.metadata.creationDate', 'ASC') - .where('photo.metadata.keywords LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) - .orWhere('photo.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('photo.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) - .orWhere('photo.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) - .innerJoinAndSelect('photo.directory', 'directory') + .createQueryBuilder('media') + .orderBy('media.metadata.creationDate', 'ASC') + .where('media.metadata.keywords LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .orWhere('media.metadata.positionData.country LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .orWhere('media.metadata.positionData.state LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .orWhere('media.metadata.positionData.city LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .orWhere('media.name LIKE :text COLLATE utf8_general_ci', {text: '%' + text + '%'}) + .innerJoinAndSelect('media.directory', 'directory') .limit(10) .getMany(); diff --git a/backend/model/sql/enitites/DirectoryEntity.ts b/backend/model/sql/enitites/DirectoryEntity.ts index 1987ec9c..ceeb3860 100644 --- a/backend/model/sql/enitites/DirectoryEntity.ts +++ b/backend/model/sql/enitites/DirectoryEntity.ts @@ -15,13 +15,13 @@ export class DirectoryEntity implements DirectoryDTO { 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') 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}) public lastScanned: number; @@ -35,6 +35,6 @@ export class DirectoryEntity implements DirectoryDTO { public directories: Array; @OneToMany(type => PhotoEntity, photo => photo.directory) - public photos: Array; + public media: Array; } diff --git a/backend/model/sql/enitites/PhotoEntity.ts b/backend/model/sql/enitites/PhotoEntity.ts index 9d510058..04f5d330 100644 --- a/backend/model/sql/enitites/PhotoEntity.ts +++ b/backend/model/sql/enitites/PhotoEntity.ts @@ -1,7 +1,8 @@ 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 {OrientationTypes} from 'ts-exif-parser'; +import {GPSMetadata, MediaDimension, PositionMetaData} from '../../../../common/entities/MediaDTO'; @Entity() export class CameraMetadataEntity implements CameraMetadata { @@ -41,7 +42,7 @@ export class GPSMetadataEntity implements GPSMetadata { } @Entity() -export class ImageSizeEntity implements ImageSize { +export class ImageSizeEntity implements MediaDimension { @Column('int') width: number; @@ -103,7 +104,7 @@ export class PhotoEntity implements PhotoDTO { @Column('text') name: string; - @ManyToOne(type => DirectoryEntity, directory => directory.photos, {onDelete: 'CASCADE'}) + @ManyToOne(type => DirectoryEntity, directory => directory.media, {onDelete: 'CASCADE'}) directory: DirectoryEntity; @Column(type => PhotoMetadataEntity) diff --git a/backend/model/threading/DiskMangerWorker.ts b/backend/model/threading/DiskMangerWorker.ts index 06a01c7c..bced1b90 100644 --- a/backend/model/threading/DiskMangerWorker.ts +++ b/backend/model/threading/DiskMangerWorker.ts @@ -1,12 +1,16 @@ import * as fs from 'fs'; import * as path from 'path'; 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 {IptcParser} from 'ts-node-iptc'; import {ExifParserFactory, OrientationTypes} from 'ts-exif-parser'; +import * as ffmpeg from 'fluent-ffmpeg'; +import {FfmpegCommand, FfprobeData} from 'fluent-ffmpeg'; import {ProjectPath} from '../../ProjectPath'; 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]'; @@ -27,6 +31,16 @@ export class DiskMangerWorker { 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 { return new Promise((resolve, reject) => { const directoryName = path.basename(relativeDirectoryName); @@ -41,9 +55,9 @@ export class DiskMangerWorker { lastScanned: Date.now(), directories: [], isPartial: false, - photos: [] + media: [] }; - fs.readdir(absoluteDirectoryName, async (err, list) => { + fs.readdir(absoluteDirectoryName, async (err, list: string[]) => { if (err) { return reject(err); } @@ -60,13 +74,23 @@ export class DiskMangerWorker { d.isPartial = true; directory.directories.push(d); } else if (DiskMangerWorker.isImage(fullFilePath)) { - directory.photos.push({ + directory.media.push({ name: file, directory: null, 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({ + name: file, + directory: null, + metadata: await DiskMangerWorker.loadPVideoMetadata(fullFilePath) + }); + + if (maxPhotos != null && directory.media.length > maxPhotos) { break; } } @@ -82,6 +106,44 @@ export class DiskMangerWorker { } + private static loadPVideoMetadata(fullPath: string): Promise { + return new Promise((resolve, reject) => { + const metadata: 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 { return new Promise((resolve, reject) => { fs.readFile(fullPath, (err, data) => { @@ -135,15 +197,15 @@ export class DiskMangerWorker { if (exif.imageSize) { - metadata.size = {width: exif.imageSize.width, height: exif.imageSize.height}; + metadata.size = {width: exif.imageSize.width, height: exif.imageSize.height}; } else if (exif.tags.RelatedImageWidth && exif.tags.RelatedImageHeight) { - metadata.size = {width: exif.tags.RelatedImageWidth, height: exif.tags.RelatedImageHeight}; + metadata.size = {width: exif.tags.RelatedImageWidth, height: exif.tags.RelatedImageHeight}; } else { - metadata.size = {width: 1, height: 1}; + metadata.size = {width: 1, height: 1}; } } catch (err) { Logger.debug(LOG_TAG, 'Error parsing exif', fullPath, err); - metadata.size = {width: 1, height: 1}; + metadata.size = {width: 1, height: 1}; } try { diff --git a/backend/model/threading/ThumbnailWorker.ts b/backend/model/threading/ThumbnailWorker.ts index a5b2c8f2..b6a3f61c 100644 --- a/backend/model/threading/ThumbnailWorker.ts +++ b/backend/model/threading/ThumbnailWorker.ts @@ -1,43 +1,103 @@ import {Metadata, Sharp} from 'sharp'; import {Dimensions, State} from 'gm'; import {Logger} from '../../Logger'; +import {FfmpegCommand, FfprobeData} from 'fluent-ffmpeg'; import {ThumbnailProcessingLib} from '../../../common/config/private/IPrivateConfig'; export class ThumbnailWorker { - private static renderer: (input: RendererInput) => Promise = null; + private static imageRenderer: (input: RendererInput) => Promise = null; + private static videoRenderer: (input: RendererInput) => Promise = null; private static rendererType = null; public static render(input: RendererInput, renderer: ThumbnailProcessingLib): Promise { + if (input.type === ThumbnailSourceType.Image) { + return this.renderFromImage(input, renderer); + } + return this.renderFromVideo(input); + } + + public static renderFromImage(input: RendererInput, renderer: ThumbnailProcessingLib): Promise { if (ThumbnailWorker.rendererType !== renderer) { - ThumbnailWorker.renderer = RendererFactory.build(renderer); + ThumbnailWorker.imageRenderer = ImageRendererFactory.build(renderer); ThumbnailWorker.rendererType = renderer; } - return ThumbnailWorker.renderer(input); + return ThumbnailWorker.imageRenderer(input); } + public static renderFromVideo(input: RendererInput): Promise { + if (ThumbnailWorker.videoRenderer === null) { + ThumbnailWorker.videoRenderer = VideoRendererFactory.build(); + } + return ThumbnailWorker.videoRenderer(input); + } + } +export enum ThumbnailSourceType { + Image, Video +} export interface RendererInput { - imagePath: string; + type: ThumbnailSourceType; + mediaPath: string; size: number; makeSquare: boolean; thPath: string; qualityPriority: boolean; } -export class RendererFactory { +export class VideoRendererFactory { + public static build(): (input: RendererInput) => Promise { + const ffmpeg = require('fluent-ffmpeg'); + return (input: RendererInput): Promise => { + 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 { switch (renderer) { case ThumbnailProcessingLib.Jimp: - return RendererFactory.Jimp(); + return ImageRendererFactory.Jimp(); case ThumbnailProcessingLib.gm: - return RendererFactory.Gm(); + return ImageRendererFactory.Gm(); case ThumbnailProcessingLib.sharp: - return RendererFactory.Sharp(); + return ImageRendererFactory.Sharp(); } throw new Error('unknown renderer'); } @@ -46,8 +106,8 @@ export class RendererFactory { const Jimp = require('jimp'); return async (input: RendererInput): Promise => { // generate thumbnail - Logger.silly('[JimpThRenderer] rendering thumbnail:' + input.imagePath); - const image = await Jimp.read(input.imagePath); + Logger.silly('[JimpThRenderer] rendering thumbnail:' + input.mediaPath); + const image = await Jimp.read(input.mediaPath); /** * newWidth * newHeight = size*size * newHeight/newWidth = height/width @@ -86,8 +146,8 @@ export class RendererFactory { const sharp = require('sharp'); return async (input: RendererInput): Promise => { - Logger.silly('[SharpThRenderer] rendering thumbnail:' + input.imagePath); - const image: Sharp = sharp(input.imagePath); + Logger.silly('[SharpThRenderer] rendering thumbnail:' + input.mediaPath); + const image: Sharp = sharp(input.mediaPath); const metadata: Metadata = await image.metadata(); /** @@ -124,8 +184,8 @@ export class RendererFactory { const gm = require('gm'); return (input: RendererInput): Promise => { return new Promise((resolve, reject) => { - Logger.silly('[GMThRenderer] rendering thumbnail:' + input.imagePath); - let image: State = gm(input.imagePath); + Logger.silly('[GMThRenderer] rendering thumbnail:' + input.mediaPath); + let image: State = gm(input.mediaPath); image.size((err, value: Dimensions) => { if (err) { return reject(err); diff --git a/backend/routes/GalleryRouter.ts b/backend/routes/GalleryRouter.ts index aec98334..a3776555 100644 --- a/backend/routes/GalleryRouter.ts +++ b/backend/routes/GalleryRouter.ts @@ -3,13 +3,16 @@ import {GalleryMWs} from '../middlewares/GalleryMWs'; import {RenderingMWs} from '../middlewares/RenderingMWs'; import {ThumbnailGeneratorMWs} from '../middlewares/thumbnail/ThumbnailGeneratorMWs'; import {UserRoles} from '../../common/entities/UserDTO'; +import {ThumbnailSourceType} from '../model/threading/ThumbnailWorker'; export class GalleryRouter { public static route(app: any) { this.addGetImageIcon(app); this.addGetImageThumbnail(app); + this.addGetVideoThumbnail(app); this.addGetImage(app); + this.addGetVideo(app); this.addRandom(app); this.addDirectoryList(app); @@ -31,10 +34,19 @@ export class GalleryRouter { 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, // 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 ); } @@ -45,27 +57,37 @@ export class GalleryRouter { AuthenticationMWs.authorise(UserRoles.Guest), // TODO: authorize path GalleryMWs.getRandomImage, - GalleryMWs.loadImage, + GalleryMWs.loadMedia, RenderingMWs.renderFile ); } 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, // TODO: authorize path - GalleryMWs.loadImage, - ThumbnailGeneratorMWs.generateThumbnail, + GalleryMWs.loadMedia, + 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 ); } 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, // TODO: authorize path - GalleryMWs.loadImage, - ThumbnailGeneratorMWs.generateIcon, + GalleryMWs.loadMedia, + ThumbnailGeneratorMWs.generateIconFactory(ThumbnailSourceType.Image), RenderingMWs.renderFile ); } diff --git a/common/entities/DirectoryDTO.ts b/common/entities/DirectoryDTO.ts index b188dd36..c6b8774f 100644 --- a/common/entities/DirectoryDTO.ts +++ b/common/entities/DirectoryDTO.ts @@ -1,4 +1,4 @@ -import {PhotoDTO} from './PhotoDTO'; +import {MediaDTO} from './MediaDTO'; export interface DirectoryDTO { id: number; @@ -9,12 +9,12 @@ export interface DirectoryDTO { isPartial?: boolean; parent: DirectoryDTO; directories: Array; - photos: Array; + media: MediaDTO[]; } export module DirectoryDTO { export const addReferences = (dir: DirectoryDTO): void => { - dir.photos.forEach((photo: PhotoDTO) => { + dir.media.forEach((photo: MediaDTO) => { photo.directory = dir; }); diff --git a/common/entities/MediaDTO.ts b/common/entities/MediaDTO.ts new file mode 100644 index 00000000..0520fbf3 --- /dev/null +++ b/common/entities/MediaDTO.ts @@ -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; + 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 (!(media).metadata.orientation) { + return false; + } + const photo = 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; + }; +} diff --git a/common/entities/PhotoDTO.ts b/common/entities/PhotoDTO.ts index 8cd73b06..d9e798da 100644 --- a/common/entities/PhotoDTO.ts +++ b/common/entities/PhotoDTO.ts @@ -1,8 +1,8 @@ import {DirectoryDTO} from './DirectoryDTO'; -import {ImageSize} from './PhotoDTO'; import {OrientationTypes} from 'ts-exif-parser'; +import {MediaDTO, MediaMetadata, MediaDimension, PositionMetaData} from './MediaDTO'; -export interface PhotoDTO { +export interface PhotoDTO extends MediaDTO { id: number; name: string; directory: DirectoryDTO; @@ -11,21 +11,16 @@ export interface PhotoDTO { readyIcon: boolean; } -export interface PhotoMetadata { +export interface PhotoMetadata extends MediaMetadata { keywords: Array; cameraData: CameraMetadata; positionData: PositionMetaData; orientation: OrientationTypes; - size: ImageSize; + size: MediaDimension; creationDate: number; fileSize: number; } -export interface ImageSize { - width: number; - height: number; -} - export interface CameraMetadata { ISO?: number; model?: string; @@ -35,50 +30,3 @@ export interface CameraMetadata { focalLength?: number; 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; - }; -} diff --git a/common/entities/SearchResultDTO.ts b/common/entities/SearchResultDTO.ts index 05cfa55b..9b4eb0ec 100644 --- a/common/entities/SearchResultDTO.ts +++ b/common/entities/SearchResultDTO.ts @@ -6,6 +6,6 @@ export interface SearchResultDTO { searchText: string; searchType: SearchTypes; directories: Array; - photos: Array; + media: Array; resultOverflow: boolean; } diff --git a/common/entities/VideoDTO.ts b/common/entities/VideoDTO.ts new file mode 100644 index 00000000..b4258d1b --- /dev/null +++ b/common/entities/VideoDTO.ts @@ -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; +} diff --git a/frontend/app/gallery/FixOrientationPipe.ts b/frontend/app/gallery/FixOrientationPipe.ts index b32fd77e..d6545724 100644 --- a/frontend/app/gallery/FixOrientationPipe.ts +++ b/frontend/app/gallery/FixOrientationPipe.ts @@ -2,7 +2,7 @@ import {Pipe, PipeTransform} from '@angular/core'; 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'}) diff --git a/frontend/app/gallery/IconPhoto.ts b/frontend/app/gallery/IconPhoto.ts deleted file mode 100644 index c0876af9..00000000 --- a/frontend/app/gallery/IconPhoto.ts +++ /dev/null @@ -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; - } -} diff --git a/frontend/app/gallery/Photo.ts b/frontend/app/gallery/Media.ts similarity index 59% rename from frontend/app/gallery/Photo.ts rename to frontend/app/gallery/Media.ts index a11aef9a..a455b594 100644 --- a/frontend/app/gallery/Photo.ts +++ b/frontend/app/gallery/Media.ts @@ -1,20 +1,21 @@ import {PhotoDTO} from '../../../common/entities/PhotoDTO'; import {Utils} from '../../../common/Utils'; -import {IconPhoto} from './IconPhoto'; +import {MediaIcon} from './MediaIcon'; 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) { - super(photo); + constructor(media: MediaDTO, public renderWidth: number, public renderHeight: number) { + super(media); } thumbnailLoaded() { if (!this.isThumbnailAvailable()) { - this.photo.readyThumbnails = this.photo.readyThumbnails || []; - this.photo.readyThumbnails.push(this.getThumbnailSize()); + this.media.readyThumbnails = this.media.readyThumbnails || []; + this.media.readyThumbnails.push(this.getThumbnailSize()); } } @@ -29,10 +30,10 @@ export class Photo extends IconPhoto { this.replacementSizeCache = null; const size = this.getThumbnailSize(); - if (!!this.photo.readyThumbnails) { - for (let i = 0; i < this.photo.readyThumbnails.length; i++) { - if (this.photo.readyThumbnails[i] < size) { - this.replacementSizeCache = this.photo.readyThumbnails[i]; + if (!!this.media.readyThumbnails) { + for (let i = 0; i < this.media.readyThumbnails.length; i++) { + if (this.media.readyThumbnails[i] < size) { + this.replacementSizeCache = this.media.readyThumbnails[i]; break; } } @@ -46,26 +47,26 @@ export class Photo extends IconPhoto { } isThumbnailAvailable() { - return this.photo.readyThumbnails && this.photo.readyThumbnails.indexOf(this.getThumbnailSize()) !== -1; + return this.media.readyThumbnails && this.media.readyThumbnails.indexOf(this.getThumbnailSize()) !== -1; } getReplacementThumbnailPath() { const size = this.getReplacementThumbnailSize(); return Utils.concatUrls(Config.Client.urlBase, '/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 { - return PhotoDTO.hasPositionData(this.photo); + return MediaDTO.hasPositionData(this.media); } getThumbnailPath() { const size = this.getThumbnailSize(); return Utils.concatUrls(Config.Client.urlBase, '/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()); } diff --git a/frontend/app/gallery/MediaIcon.ts b/frontend/app/gallery/MediaIcon.ts new file mode 100644 index 00000000..f3043e36 --- /dev/null +++ b/frontend/app/gallery/MediaIcon.ts @@ -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; + } +} diff --git a/frontend/app/gallery/cache.gallery.service.ts b/frontend/app/gallery/cache.gallery.service.ts index 4808590c..642e2880 100644 --- a/frontend/app/gallery/cache.gallery.service.ts +++ b/frontend/app/gallery/cache.gallery.service.ts @@ -5,6 +5,7 @@ import {Utils} from '../../../common/Utils'; import {Config} from '../../../common/config/public/Config'; import {AutoCompleteItem, SearchTypes} from '../../../common/entities/AutoCompleteItem'; import {SearchResultDTO} from '../../../common/entities/SearchResultDTO'; +import {MediaDTO} from '../../../common/entities/MediaDTO'; interface CacheItem { timestamp: number; @@ -132,24 +133,24 @@ export class GalleryCacheService { } /** - * Update photo state at cache too (Eg.: thumbnail rendered) - * @param photo + * Update media state at cache too (Eg.: thumbnail rendered) + * @param media */ - public photoUpdated(photo: PhotoDTO): void { + public mediaUpdated(media: MediaDTO): void { if (Config.Client.Other.enableCache === false) { 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); if (value != null) { const directory: DirectoryDTO = JSON.parse(value); - directory.photos.forEach((p) => { - if (p.name === photo.name) { + directory.media.forEach((p) => { + if (p.name === media.name) { // update data - p.metadata = photo.metadata; - p.readyThumbnails = photo.readyThumbnails; + p.metadata = media.metadata; + p.readyThumbnails = media.readyThumbnails; // save changes localStorage.setItem(directoryName, JSON.stringify(directory)); diff --git a/frontend/app/gallery/directory/directory.gallery.component.css b/frontend/app/gallery/directory/directory.gallery.component.css index 91e7e9dd..5d0218c4 100644 --- a/frontend/app/gallery/directory/directory.gallery.component.css +++ b/frontend/app/gallery/directory/directory.gallery.component.css @@ -55,7 +55,7 @@ a:hover .photo-container { white-space: normal; } -/* transforming photo, based on exif orientation*/ +/* transforming media, based on exif orientation*/ .photo-orientation-1 { } .photo-orientation-2 { diff --git a/frontend/app/gallery/directory/directory.gallery.component.ts b/frontend/app/gallery/directory/directory.gallery.component.ts index 4342a9ee..64007415 100644 --- a/frontend/app/gallery/directory/directory.gallery.component.ts +++ b/frontend/app/gallery/directory/directory.gallery.component.ts @@ -3,11 +3,12 @@ import {DomSanitizer} from '@angular/platform-browser'; import {DirectoryDTO} from '../../../../common/entities/DirectoryDTO'; import {RouterLink} from '@angular/router'; import {Utils} from '../../../../common/Utils'; -import {Photo} from '../Photo'; +import {Media} from '../Media'; import {Thumbnail, ThumbnailManagerService} from '../thumnailManager.service'; import {PageHelper} from '../../model/page.helper'; import {QueryService} from '../../model/query.service'; import {PhotoDTO} from '../../../../common/entities/PhotoDTO'; +import {MediaDTO} from '../../../../common/entities/MediaDTO'; @Component({ selector: 'app-gallery-directory', @@ -28,9 +29,9 @@ export class GalleryDirectoryComponent implements OnInit, OnDestroy { size: number = null; - public get SamplePhoto(): PhotoDTO { - if (this.directory.photos.length > 0) { - return this.directory.photos[0]; + public get SamplePhoto(): MediaDTO { + if (this.directory.media.length > 0) { + return this.directory.media[0]; } return null; } @@ -57,8 +58,8 @@ export class GalleryDirectoryComponent implements OnInit, OnDestroy { } ngOnInit() { - if (this.directory.photos.length > 0) { - this.thumbnail = this.thumbnailService.getThumbnail(new Photo(this.SamplePhoto, this.calcSize(), this.calcSize())); + if (this.directory.media.length > 0) { + this.thumbnail = this.thumbnailService.getThumbnail(new Media(this.SamplePhoto, this.calcSize(), this.calcSize())); } } diff --git a/frontend/app/gallery/gallery.component.html b/frontend/app/gallery/gallery.component.html index f94bee76..a5f799f8 100644 --- a/frontend/app/gallery/gallery.component.html +++ b/frontend/app/gallery/gallery.component.html @@ -34,8 +34,8 @@ [directory]="directory"> - + @@ -58,13 +58,13 @@ + [photos]="_galleryService.content.value.searchResult.media">
- diff --git a/frontend/app/gallery/gallery.component.ts b/frontend/app/gallery/gallery.component.ts index 7381d97b..fdab04d7 100644 --- a/frontend/app/gallery/gallery.component.ts +++ b/frontend/app/gallery/gallery.component.ts @@ -122,16 +122,16 @@ export class GalleryComponent implements OnInit, OnDestroy { const tmp = (content.searchResult || content.directory || { directories: [], - photos: [] + media: [] }); this.directories = tmp.directories; this.sortDirectories(); this.isPhotoWithLocation = false; - for (let i = 0; i < tmp.photos.length; i++) { - if (tmp.photos[i].metadata && - tmp.photos[i].metadata.positionData && - tmp.photos[i].metadata.positionData.GPSData && - tmp.photos[i].metadata.positionData.GPSData.longitude + for (let i = 0; i < tmp.media.length; i++) { + if (tmp.media[i].metadata && + tmp.media[i].metadata.positionData && + tmp.media[i].metadata.positionData.GPSData && + tmp.media[i].metadata.positionData.GPSData.longitude ) { this.isPhotoWithLocation = true; break; diff --git a/frontend/app/gallery/gallery.service.ts b/frontend/app/gallery/gallery.service.ts index def4d1a7..609e4d85 100644 --- a/frontend/app/gallery/gallery.service.ts +++ b/frontend/app/gallery/gallery.service.ts @@ -147,7 +147,7 @@ export class GalleryService { this.content.next(cw); // 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) { clearTimeout(this.searchId); } diff --git a/frontend/app/gallery/grid/GridMedia.ts b/frontend/app/gallery/grid/GridMedia.ts new file mode 100644 index 00000000..098588c5 --- /dev/null +++ b/frontend/app/gallery/grid/GridMedia.ts @@ -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 (this.media).metadata.orientation || OrientationTypes.TOP_LEFT; + } + + isPhoto(): boolean { + return typeof (this.media).metadata.cameraData !== 'undefined'; + } + + isVideo(): boolean { + return typeof (this.media).metadata.cameraData === 'undefined'; + } + + +} diff --git a/frontend/app/gallery/grid/GridPhoto.ts b/frontend/app/gallery/grid/GridPhoto.ts deleted file mode 100644 index bd09e12e..00000000 --- a/frontend/app/gallery/grid/GridPhoto.ts +++ /dev/null @@ -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); - } - - -} diff --git a/frontend/app/gallery/grid/GridRowBuilder.ts b/frontend/app/gallery/grid/GridRowBuilder.ts index 2968ce9e..e43d01fd 100644 --- a/frontend/app/gallery/grid/GridRowBuilder.ts +++ b/frontend/app/gallery/grid/GridRowBuilder.ts @@ -1,10 +1,11 @@ import {PhotoDTO} from '../../../../common/entities/PhotoDTO'; +import {MediaDTO} from '../../../../common/entities/MediaDTO'; export class GridRowBuilder { 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[], @@ -52,7 +53,7 @@ export class GridRowBuilder { 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) { this.addPhoto(); } @@ -61,7 +62,7 @@ export class GridRowBuilder { public calcRowHeight(): number { let width = 0; 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 } const height = (this.containerWidth - this.photoRow.length * (this.photoMargin * 2) - 1) / width; // cant be equal -> width-1 diff --git a/frontend/app/gallery/grid/grid.gallery.component.html b/frontend/app/gallery/grid/grid.gallery.component.html index c4b4a821..8881a500 100644 --- a/frontend/app/gallery/grid/grid.gallery.component.html +++ b/frontend/app/gallery/grid/grid.gallery.component.html @@ -3,7 +3,7 @@ *ngFor="let gridPhoto of photosToRender" [routerLink]="[]" - [queryParams]="queryService.getParams(gridPhoto.photo)" + [queryParams]="queryService.getParams(gridPhoto.media)" [gridPhoto]="gridPhoto" [style.width.px]="gridPhoto.renderWidth" [style.height.px]="gridPhoto.renderHeight" diff --git a/frontend/app/gallery/grid/grid.gallery.component.ts b/frontend/app/gallery/grid/grid.gallery.component.ts index 59eb8531..1ac9c027 100644 --- a/frontend/app/gallery/grid/grid.gallery.component.ts +++ b/frontend/app/gallery/grid/grid.gallery.component.ts @@ -15,7 +15,7 @@ import { import {PhotoDTO} from '../../../../common/entities/PhotoDTO'; import {GridRowBuilder} from './GridRowBuilder'; import {GalleryLightboxComponent} from '../lightbox/lightbox.gallery.component'; -import {GridPhoto} from './GridPhoto'; +import {GridMedia} from './GridMedia'; import {GalleryPhotoComponent} from './photo/photo.grid.gallery.component'; import {OverlayService} from '../overlay.service'; 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 {GalleryService} from '../gallery.service'; import {SortingMethods} from '../../../../common/entities/SortingMethods'; +import {MediaDTO} from '../../../../common/entities/MediaDTO'; @Component({ selector: 'app-gallery-grid', @@ -40,7 +41,7 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O @Input() photos: Array; @Input() lightbox: GalleryLightboxComponent; - photosToRender: Array = []; + photosToRender: Array = []; containerWidth = 0; screenHeight = 0; @@ -189,8 +190,8 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O const imageHeight = rowHeight - (this.IMAGE_MARGIN * 2); photoRowBuilder.getPhotoRow().forEach((photo) => { - const imageWidth = imageHeight * PhotoDTO.calcRotatedAspectRatio(photo); - this.photosToRender.push(new GridPhoto(photo, imageWidth, imageHeight, this.renderedPhotoIndex)); + const imageWidth = imageHeight * MediaDTO.calcRotatedAspectRatio(photo); + this.photosToRender.push(new GridMedia(photo, imageWidth, imageHeight, this.renderedPhotoIndex)); }); this.renderedPhotoIndex += photoRowBuilder.getPhotoRow().length; @@ -248,7 +249,7 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O let lastRowId = null; 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) { lastSameIndex = i; lastRowId = this.photosToRender[i].rowId; @@ -316,7 +317,7 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O this.renderedPhotoIndex < numberOfPhotos)) { const ret = this.renderARow(); if (ret === null) { - throw new Error('Grid photos rendering failed'); + throw new Error('Grid media rendering failed'); } renderedContentHeight += ret; } diff --git a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.html b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.html index 1b62cc00..784ea3c3 100644 --- a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.html +++ b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.html @@ -1,5 +1,5 @@
- -
{{gridPhoto.photo.name}}
+
{{gridPhoto.media.name}}
@@ -27,8 +27,8 @@
-
- +
+ #{{keyword}} #{{keyword}} diff --git a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.ts b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.ts index 60da3393..ddb8f0ef 100644 --- a/frontend/app/gallery/grid/photo/photo.grid.gallery.component.ts +++ b/frontend/app/gallery/grid/photo/photo.grid.gallery.component.ts @@ -1,6 +1,6 @@ import {Component, ElementRef, Input, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {Dimension, IRenderable} from '../../../model/IRenderable'; -import {GridPhoto} from '../GridPhoto'; +import {GridMedia} from '../GridMedia'; import {SearchTypes} from '../../../../../common/entities/AutoCompleteItem'; import {RouterLink} from '@angular/router'; import {Thumbnail, ThumbnailManagerService} from '../../thumnailManager.service'; @@ -15,7 +15,7 @@ import {PageHelper} from '../../../model/page.helper'; providers: [RouterLink] }) export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy { - @Input() gridPhoto: GridPhoto; + @Input() gridPhoto: GridMedia; @ViewChild('img') imageRef: ElementRef; @ViewChild('info') infoDiv: ElementRef; @ViewChild('photoContainer') container: ElementRef; @@ -78,9 +78,9 @@ export class GalleryPhotoComponent implements IRenderable, OnInit, OnDestroy { if (!this.gridPhoto) { return ''; } - return this.gridPhoto.photo.metadata.positionData.city || - this.gridPhoto.photo.metadata.positionData.state || - this.gridPhoto.photo.metadata.positionData.country; + return this.gridPhoto.media.metadata.positionData.city || + this.gridPhoto.media.metadata.positionData.state || + this.gridPhoto.media.metadata.positionData.country; } diff --git a/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.html b/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.html index f19c7963..4b46e0e5 100644 --- a/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.html +++ b/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.html @@ -11,12 +11,12 @@
- {{photo.name}} + {{media.name}}
-
{{photo.metadata.size.width}} x {{photo.metadata.size.height}}
+
{{media.metadata.size.width}} x {{media.metadata.size.height}}
{{calcMpx()}}MP
-
{{calcFileSize()}}
+
{{calcFileSize()}}
@@ -27,32 +27,32 @@
- {{ photo.metadata.creationDate | date: (isThisYear() ? 'MMMM d': 'longDate')}} + {{ media.metadata.creationDate | date: (isThisYear() ? 'MMMM d': 'longDate')}}
-
{{ photo.metadata.creationDate | date :'EEEE'}}, {{getTime()}}
+
{{ media.metadata.creationDate | date :'EEEE'}}, {{getTime()}}
-
+
- {{photo.metadata.cameraData.model || photo.metadata.cameraData.make || "Camera"}} + {{CameraData.model || CameraData.make || "Camera"}}
-
ISO{{photo.metadata.cameraData.ISO}}
-
f/{{photo.metadata.cameraData.fStop}}
-
- {{toFraction(photo.metadata.cameraData.exposure)}}s +
ISO{{CameraData.ISO}}
+
f/{{CameraData.fStop}}
+
+ {{toFraction(CameraData.exposure)}}s
-
- {{photo.metadata.cameraData.focalLength}}mm +
+ {{CameraData.focalLength}}mm
-
{{photo.metadata.cameraData.lens}}
+
{{CameraData.lens}}
@@ -67,8 +67,8 @@
- {{photo.metadata.positionData.GPSData.latitude.toFixed(3)}}, - {{photo.metadata.positionData.GPSData.longitude.toFixed(3)}} + {{media.metadata.positionData.GPSData.latitude.toFixed(3)}}, + {{media.metadata.positionData.GPSData.longitude.toFixed(3)}}
@@ -80,11 +80,11 @@ [zoomControl]="false" [streetViewControl]="false" [zoom]="10" - [latitude]="photo.metadata.positionData.GPSData.latitude" - [longitude]="photo.metadata.positionData.GPSData.longitude"> + [latitude]="media.metadata.positionData.GPSData.latitude" + [longitude]="media.metadata.positionData.GPSData.longitude"> + [latitude]="media.metadata.positionData.GPSData.latitude" + [longitude]="media.metadata.positionData.GPSData.longitude">
diff --git a/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts b/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts index dabce203..30782017 100644 --- a/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts +++ b/frontend/app/gallery/lightbox/infopanel/info-panel.lightbox.gallery.component.ts @@ -1,6 +1,7 @@ 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 {MediaDTO} from '../../../../../common/entities/MediaDTO'; @Component({ selector: 'app-info-panel', @@ -8,7 +9,7 @@ import {Config} from '../../../../../common/config/public/Config'; templateUrl: './info-panel.lightbox.gallery.component.html', }) export class InfoPanelLightboxComponent { - @Input() photo: PhotoDTO; + @Input() media: MediaDTO; @Output('onClose') onClose = new EventEmitter(); public mapEnabled = true; @@ -17,13 +18,13 @@ export class InfoPanelLightboxComponent { } 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() { const postFixes = ['B', 'KB', 'MB', 'GB', 'TB']; let index = 0; - let size = this.photo.metadata.fileSize; + let size = this.media.metadata.fileSize; while (size > 1000 && index < postFixes.length - 1) { size /= 1000; index++; @@ -33,12 +34,12 @@ export class InfoPanelLightboxComponent { isThisYear() { return (new Date()).getFullYear() === - (new Date(this.photo.metadata.creationDate)).getFullYear(); + (new Date(this.media.metadata.creationDate)).getFullYear(); } getTime() { - const date = new Date(this.photo.metadata.creationDate); + const date = new Date(this.media.metadata.creationDate); return date.toTimeString().split(' ')[0]; } @@ -51,29 +52,33 @@ export class InfoPanelLightboxComponent { } hasPositionData(): boolean { - return PhotoDTO.hasPositionData(this.photo); + return MediaDTO.hasPositionData(this.media); } hasGPS() { - return this.photo.metadata.positionData && this.photo.metadata.positionData.GPSData && - this.photo.metadata.positionData.GPSData.latitude && this.photo.metadata.positionData.GPSData.longitude; + return this.media.metadata.positionData && this.media.metadata.positionData.GPSData && + this.media.metadata.positionData.GPSData.latitude && this.media.metadata.positionData.GPSData.longitude; } getPositionText(): string { - if (!this.photo.metadata.positionData) { + if (!this.media.metadata.positionData) { return ''; } - let str = this.photo.metadata.positionData.city || - this.photo.metadata.positionData.state || ''; + let str = this.media.metadata.positionData.city || + this.media.metadata.positionData.state || ''; if (str.length !== 0) { str += ', '; } - str += this.photo.metadata.positionData.country || ''; + str += this.media.metadata.positionData.country || ''; return str; } + get CameraData(): CameraMetadata { + return (this.media).metadata.cameraData; + } + close() { this.onClose.emit(); } diff --git a/frontend/app/gallery/lightbox/lightbox.gallery.component.html b/frontend/app/gallery/lightbox/lightbox.gallery.component.html index fcfc864f..c82f1600 100644 --- a/frontend/app/gallery/lightbox/lightbox.gallery.component.html +++ b/frontend/app/gallery/lightbox/lightbox.gallery.component.html @@ -5,8 +5,8 @@ diff --git a/frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.ts b/frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.ts index f9693e19..efa034fd 100644 --- a/frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.ts +++ b/frontend/app/gallery/lightbox/photo/photo.lightbox.gallery.component.ts @@ -1,7 +1,8 @@ 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 {FixOrientationPipe} from '../../FixOrientationPipe'; +import {MediaDTO} from '../../../../../common/entities/MediaDTO'; @Component({ selector: 'app-gallery-lightbox-photo', @@ -10,8 +11,8 @@ import {FixOrientationPipe} from '../../FixOrientationPipe'; }) export class GalleryLightboxPhotoComponent implements OnChanges { - @Input() gridPhoto: GridPhoto; - @Input() loadImage = false; + @Input() gridMedia: GridMedia; + @Input() loadMedia = false; @Input() windowAspect = 1; prevGirdPhoto = null; @@ -30,28 +31,33 @@ export class GalleryLightboxPhotoComponent implements OnChanges { this.imageLoaded = false; this.imageLoadFinished = false; this.setImageSize(); - if (this.prevGirdPhoto !== this.gridPhoto) { - this.prevGirdPhoto = this.gridPhoto; + if (this.prevGirdPhoto !== this.gridMedia) { + this.prevGirdPhoto = this.gridMedia; this.thumbnailSrc = null; this.photoSrc = null; } - if (this.thumbnailSrc == null && this.gridPhoto && this.ThumbnailUrl !== null) { - FixOrientationPipe.transform(this.ThumbnailUrl, this.gridPhoto.photo.metadata.orientation) + if (this.thumbnailSrc == null && this.gridMedia && this.ThumbnailUrl !== null) { + FixOrientationPipe.transform(this.ThumbnailUrl, this.gridMedia.Orientation) .then((src) => this.thumbnailSrc = src); } - if (this.photoSrc == null && this.gridPhoto && this.loadImage) { - FixOrientationPipe.transform(this.gridPhoto.getPhotoPath(), this.gridPhoto.photo.metadata.orientation) - .then((src) => this.thumbnailSrc = src); + if (this.photoSrc == null && this.gridMedia && this.loadMedia) { + FixOrientationPipe.transform(this.gridMedia.getPhotoPath(), this.gridMedia.Orientation) + .then((src) => this.photoSrc = src); } } onImageError() { // TODO:handle error 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() { this.imageLoadFinished = true; @@ -59,34 +65,34 @@ export class GalleryLightboxPhotoComponent implements OnChanges { } private get ThumbnailUrl(): string { - if (this.gridPhoto.isThumbnailAvailable() === true) { - return this.gridPhoto.getThumbnailPath(); + if (this.gridMedia.isThumbnailAvailable() === true) { + return this.gridMedia.getThumbnailPath(); } - if (this.gridPhoto.isReplacementThumbnailAvailable() === true) { - return this.gridPhoto.getReplacementThumbnailPath(); + if (this.gridMedia.isReplacementThumbnailAvailable() === true) { + return this.gridMedia.getReplacementThumbnailPath(); } return null; } public get PhotoSrc(): string { - return this.gridPhoto.getPhotoPath(); + return this.gridMedia.getPhotoPath(); } public showThumbnail(): boolean { - return this.gridPhoto && + return this.gridMedia && !this.imageLoaded && this.thumbnailSrc !== null && - (this.gridPhoto.isThumbnailAvailable() || this.gridPhoto.isReplacementThumbnailAvailable()); + (this.gridMedia.isThumbnailAvailable() || this.gridMedia.isReplacementThumbnailAvailable()); } private setImageSize() { - if (!this.gridPhoto) { + if (!this.gridMedia) { return; } - const photoAspect = PhotoDTO.calcRotatedAspectRatio(this.gridPhoto.photo); + const photoAspect = MediaDTO.calcRotatedAspectRatio(this.gridMedia.media); if (photoAspect < this.windowAspect) { this.imageSize.height = '100'; diff --git a/frontend/app/gallery/map/lightbox/lightbox.map.gallery.component.ts b/frontend/app/gallery/map/lightbox/lightbox.map.gallery.component.ts index aabcfb14..93d519f3 100644 --- a/frontend/app/gallery/map/lightbox/lightbox.map.gallery.component.ts +++ b/frontend/app/gallery/map/lightbox/lightbox.map.gallery.component.ts @@ -4,10 +4,11 @@ import {Dimension} from '../../../model/IRenderable'; import {FullScreenService} from '../../fullscreen.service'; import {AgmMap, LatLngBounds, MapsAPILoader} from '@agm/core'; import {IconThumbnail, Thumbnail, ThumbnailManagerService} from '../../thumnailManager.service'; -import {IconPhoto} from '../../IconPhoto'; -import {Photo} from '../../Photo'; +import {MediaIcon} from '../../MediaIcon'; +import {Media} from '../../Media'; import {PageHelper} from '../../../model/page.helper'; import {OrientationTypes} from 'ts-exif-parser'; +import {MediaDTO} from '../../../../../common/entities/MediaDTO'; @Component({ @@ -108,13 +109,13 @@ export class GalleryMapLightboxComponent implements OnChanges, AfterViewInit { }).map(p => { let width = 500; let height = 500; - const rotatedSize = PhotoDTO.getRotatedSize(p); + const rotatedSize = MediaDTO.getRotatedSize(p); if (rotatedSize.width > rotatedSize.height) { height = width * (rotatedSize.height / rotatedSize.width); } else { 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; const obj: MapPhoto = { latitude: p.metadata.positionData.GPSData.latitude, @@ -124,7 +125,7 @@ export class GalleryMapLightboxComponent implements OnChanges, AfterViewInit { preview: { width: width, height: height, - thumbnail: this.thumbnailService.getLazyThumbnail(new Photo(p, width, height)) + thumbnail: this.thumbnailService.getLazyThumbnail(new Media(p, width, height)) } }; diff --git a/frontend/app/gallery/navigator/navigator.gallery.component.html b/frontend/app/gallery/navigator/navigator.gallery.component.html index 33818ca3..8deb2b16 100644 --- a/frontend/app/gallery/navigator/navigator.gallery.component.html +++ b/frontend/app/gallery/navigator/navigator.gallery.component.html @@ -8,10 +8,10 @@
-
- {{directory.photos.length}} items +
+ {{directory.media.length}} items
-
 
+