diff --git a/angular.json b/angular.json index 2558d276..0d753844 100644 --- a/angular.json +++ b/angular.json @@ -23,6 +23,7 @@ "node_modules/ngx-toastr/toastr.css", "node_modules/bootstrap/dist/css/bootstrap.css", "node_modules/open-iconic/font/css/open-iconic-bootstrap.css", + "node_modules/ngx-bootstrap/datepicker/bs-datepicker.css", "frontend/styles.css" ], "scripts": [] diff --git a/backend/middlewares/AdminMWs.ts b/backend/middlewares/AdminMWs.ts index a2893249..9ddf9c85 100644 --- a/backend/middlewares/AdminMWs.ts +++ b/backend/middlewares/AdminMWs.ts @@ -103,6 +103,28 @@ export class AdminMWs { } + public static async updateRandomPhotoSettings(req: Request, res: Response, next: NextFunction) { + if ((typeof req.body === 'undefined') || (typeof req.body.settings === 'undefined')) { + return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'settings is needed')); + } + + try { + // only updating explicitly set config (not saving config set by the diagnostics) + const original = Config.original(); + await ConfigDiagnostics.testRandomPhotoConfig(req.body.settings, original); + + Config.Client.RandomPhoto = req.body.settings; + original.Client.RandomPhoto = req.body.settings; + original.save(); + await ConfigDiagnostics.runDiagnostics(); + Logger.info(LOG_TAG, 'new config:'); + Logger.info(LOG_TAG, JSON.stringify(Config, null, '\t')); + return next(); + } catch (err) { + return next(new ErrorDTO(ErrorCodes.SETTINGS_ERROR, 'Settings error: ' + JSON.stringify(err, null, ' '), err)); + } + } + public static async updateSearchSettings(req: Request, res: Response, next: NextFunction) { if ((typeof req.body === 'undefined') || (typeof req.body.settings === 'undefined')) { return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'settings is needed')); diff --git a/backend/middlewares/GalleryMWs.ts b/backend/middlewares/GalleryMWs.ts index 6e767440..9960d767 100644 --- a/backend/middlewares/GalleryMWs.ts +++ b/backend/middlewares/GalleryMWs.ts @@ -10,6 +10,7 @@ import {PhotoDTO} from '../../common/entities/PhotoDTO'; import {ProjectPath} from '../ProjectPath'; import {Config} from '../../common/config/private/Config'; import {UserDTO} from '../../common/entities/UserDTO'; +import {RandomQuery} from '../model/interfaces/IGalleryManager'; const LOG_TAG = '[GalleryMWs]'; @@ -79,6 +80,50 @@ export class GalleryMWs { } + public static async getRandomImage(req: Request, res: Response, next: NextFunction) { + if (Config.Client.RandomPhoto.enabled === false) { + return next(); + } + const query: RandomQuery = {}; + if (req.query.directory) { + query.directory = req.query.directory; + } + if (req.query.recursive === 'true') { + query.recursive = true; + } + if (req.query.orientation) { + query.orientation = parseInt(req.query.orientation.toString(), 10); + } + if (req.query.maxResolution) { + query.maxResolution = parseFloat(req.query.maxResolution.toString()); + } + if (req.query.minResolution) { + query.minResolution = parseFloat(req.query.minResolution.toString()); + } + if (req.query.fromDate) { + query.fromDate = new Date(req.query.fromDate); + } + if (req.query.toDate) { + query.toDate = new Date(req.query.toDate); + } + if (query.minResolution && query.maxResolution && query.maxResolution < query.minResolution) { + return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'Input error: min resolution is greater than the max resolution')); + } + if (query.toDate && query.fromDate && query.toDate.getTime() < query.fromDate.getTime()) { + return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'Input error: to date is earlier than from date')); + } + + const photo = await ObjectManagerRepository.getInstance() + .GalleryManager.getRandomPhoto(query); + if (!photo) { + return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'No photo found')); + } + + req.params.imagePath = 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)) { return next(); @@ -134,7 +179,7 @@ export class GalleryMWs { } 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 || []); req.resultPipe = new ContentWrapper(null, result); diff --git a/backend/middlewares/SharingMWs.ts b/backend/middlewares/SharingMWs.ts index 99099b68..af0486fa 100644 --- a/backend/middlewares/SharingMWs.ts +++ b/backend/middlewares/SharingMWs.ts @@ -2,6 +2,7 @@ import {NextFunction, Request, Response} from 'express'; import {CreateSharingDTO, SharingDTO} from '../../common/entities/SharingDTO'; import {ObjectManagerRepository} from '../model/ObjectManagerRepository'; import {ErrorCodes, ErrorDTO} from '../../common/entities/Error'; +import {Config} from '../../common/config/private/Config'; const LOG_TAG = '[SharingMWs]'; @@ -20,6 +21,9 @@ export class SharingMWs { public static async getSharing(req: Request, res: Response, next: NextFunction) { + if (Config.Client.Sharing.enabled === false) { + return next(); + } const sharingKey = req.params.sharingKey; try { @@ -33,6 +37,9 @@ export class SharingMWs { } public static async createSharing(req: Request, res: Response, next: NextFunction) { + if (Config.Client.Sharing.enabled === false) { + return next(); + } if ((typeof req.body === 'undefined') || (typeof req.body.createSharing === 'undefined')) { return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'createSharing filed is missing')); } @@ -75,6 +82,9 @@ export class SharingMWs { } public static async updateSharing(req: Request, res: Response, next: NextFunction) { + if (Config.Client.Sharing.enabled === false) { + return next(); + } if ((typeof req.body === 'undefined') || (typeof req.body.updateSharing === 'undefined')) { return next(new ErrorDTO(ErrorCodes.INPUT_ERROR, 'updateSharing filed is missing')); } diff --git a/backend/model/ConfigDiagnostics.ts b/backend/model/ConfigDiagnostics.ts index 1099605b..6fced012 100644 --- a/backend/model/ConfigDiagnostics.ts +++ b/backend/model/ConfigDiagnostics.ts @@ -110,6 +110,14 @@ export class ConfigDiagnostics { } } + static async testRandomPhotoConfig(sharing: ClientConfig.RandomPhotoConfig, config: IPrivateConfig) { + if (sharing.enabled === true && + config.Server.database.type === DatabaseType.memory) { + throw new Error('Memory Database do not support sharing'); + } + } + + static async testMapConfig(map: ClientConfig.MapConfig) { if (map.enabled === true && (!map.googleApiKey || map.googleApiKey.length === 0)) { throw new Error('Maps need a valid google api key'); @@ -192,6 +200,17 @@ export class ConfigDiagnostics { Config.Client.Sharing.enabled = false; } + try { + 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. ' + + 'Please adjust the config properly.', err.toString()); + Logger.warn(LOG_TAG, 'Random Photo is not supported with these settings, switching off..', err.toString()); + Config.Client.Sharing.enabled = false; + } + + try { await ConfigDiagnostics.testMapConfig(Config.Client.Map); } catch (ex) { diff --git a/backend/model/interfaces/IGalleryManager.ts b/backend/model/interfaces/IGalleryManager.ts index 100ff4d2..2094f7d6 100644 --- a/backend/model/interfaces/IGalleryManager.ts +++ b/backend/model/interfaces/IGalleryManager.ts @@ -1,8 +1,22 @@ import {DirectoryDTO} from '../../../common/entities/DirectoryDTO'; +import {PhotoDTO} from '../../../common/entities/PhotoDTO'; +import {OrientationType, RandomQueryDTO} from '../../../common/entities/RandomQueryDTO'; + +export interface RandomQuery { + directory?: string; + recursive?: boolean; + orientation?: OrientationType; + fromDate?: Date; + toDate?: Date; + minResolution?: number; + maxResolution?: number; +} export interface IGalleryManager { listDirectory(relativeDirectoryName: string, knownLastModified?: number, knownLastScanned?: number): Promise; + getRandomPhoto(queryFilter: RandomQuery): Promise; + } diff --git a/backend/model/memory/GalleryManager.ts b/backend/model/memory/GalleryManager.ts index 532b2876..d6e36ac7 100644 --- a/backend/model/memory/GalleryManager.ts +++ b/backend/model/memory/GalleryManager.ts @@ -6,6 +6,7 @@ import {DiskManager} from '../DiskManger'; import {ProjectPath} from '../../ProjectPath'; import {Config} from '../../../common/config/private/Config'; import {ReIndexingSensitivity} from '../../../common/config/private/IPrivateConfig'; +import {PhotoDTO} from '../../../common/entities/PhotoDTO'; export class GalleryManager implements IGalleryManager { @@ -23,4 +24,7 @@ export class GalleryManager implements IGalleryManager { return DiskManager.scanDirectory(relativeDirectoryName); } + getRandomPhoto(RandomQuery): Promise { + throw new Error('Random photo is not supported without database'); + } } diff --git a/backend/model/sql/GalleryManager.ts b/backend/model/sql/GalleryManager.ts index 8d55eac6..27a7f542 100644 --- a/backend/model/sql/GalleryManager.ts +++ b/backend/model/sql/GalleryManager.ts @@ -1,4 +1,4 @@ -import {IGalleryManager} from '../interfaces/IGalleryManager'; +import {IGalleryManager, RandomQuery} from '../interfaces/IGalleryManager'; import {DirectoryDTO} from '../../../common/entities/DirectoryDTO'; import * as path from 'path'; import * as fs from 'fs'; @@ -11,6 +11,9 @@ import {ProjectPath} from '../../ProjectPath'; import {Config} from '../../../common/config/private/Config'; import {ISQLGalleryManager} from './IGalleryManager'; import {ReIndexingSensitivity} from '../../../common/config/private/IPrivateConfig'; +import {PhotoDTO} from '../../../common/entities/PhotoDTO'; +import {OrientationType} from '../../../common/entities/RandomQueryDTO'; +import {Brackets} from 'typeorm'; export class GalleryManager implements IGalleryManager, ISQLGalleryManager { @@ -226,4 +229,61 @@ export class GalleryManager implements IGalleryManager, ISQLGalleryManager { } + async getRandomPhoto(queryFilter: RandomQuery): Promise { + const connection = await SQLConnection.getConnection(); + const photosRepository = connection.getRepository(PhotoEntity); + + const query = photosRepository.createQueryBuilder('photo'); + query.innerJoinAndSelect('photo.directory', 'directory'); + + if (queryFilter.directory) { + const directoryName = path.basename(queryFilter.directory); + const directoryParent = path.join(path.dirname(queryFilter.directory), path.sep); + + query.where(new Brackets(qb => { + qb.where('directory.name = :name AND directory.path = :path', { + name: directoryName, + path: directoryParent + }); + + if (queryFilter.recursive) { + qb.orWhere('directory.name LIKE :text COLLATE utf8_general_ci', {text: '%' + queryFilter.directory + '%'}); + } + })); + } + + if (queryFilter.fromDate) { + query.andWhere('photo.metadata.creationDate >= :fromDate', { + fromDate: queryFilter.fromDate.getTime() + }); + } + if (queryFilter.toDate) { + query.andWhere('photo.metadata.creationDate <= :toDate', { + toDate: queryFilter.toDate.getTime() + }); + } + if (queryFilter.minResolution) { + query.andWhere('photo.metadata.size.width * photo.metadata.size.height >= :minRes', { + minRes: queryFilter.minResolution * 1000 * 1000 + }); + } + + if (queryFilter.maxResolution) { + query.andWhere('photo.metadata.size.width * photo.metadata.size.height <= :maxRes', { + maxRes: queryFilter.maxResolution * 1000 * 1000 + }); + } + if (queryFilter.orientation === OrientationType.landscape) { + query.andWhere('photo.metadata.size.width >= photo.metadata.size.height'); + } + if (queryFilter.orientation === OrientationType.portrait) { + query.andWhere('photo.metadata.size.width <= photo.metadata.size.height'); + } + + + return await query.groupBy('RANDOM()').limit(1).getOne(); + + } + + } diff --git a/backend/model/sql/IGalleryManager.ts b/backend/model/sql/IGalleryManager.ts index b2f72202..7f0b6ad8 100644 --- a/backend/model/sql/IGalleryManager.ts +++ b/backend/model/sql/IGalleryManager.ts @@ -1,6 +1,7 @@ import {DirectoryDTO} from '../../../common/entities/DirectoryDTO'; +import {IGalleryManager} from '../interfaces/IGalleryManager'; -export interface ISQLGalleryManager { +export interface ISQLGalleryManager extends IGalleryManager{ listDirectory(relativeDirectoryName: string, knownLastModified?: number, knownLastScanned?: number): Promise; diff --git a/backend/model/threading/ThumbnailWorker.ts b/backend/model/threading/ThumbnailWorker.ts index a9cdf437..a5b2c8f2 100644 --- a/backend/model/threading/ThumbnailWorker.ts +++ b/backend/model/threading/ThumbnailWorker.ts @@ -1,4 +1,4 @@ -import {Metadata, SharpInstance} from 'sharp'; +import {Metadata, Sharp} from 'sharp'; import {Dimensions, State} from 'gm'; import {Logger} from '../../Logger'; import {ThumbnailProcessingLib} from '../../../common/config/private/IPrivateConfig'; @@ -87,7 +87,7 @@ export class RendererFactory { return async (input: RendererInput): Promise => { Logger.silly('[SharpThRenderer] rendering thumbnail:' + input.imagePath); - const image: SharpInstance = sharp(input.imagePath); + const image: Sharp = sharp(input.imagePath); const metadata: Metadata = await image.metadata(); /** @@ -110,9 +110,10 @@ export class RendererFactory { } else { image .resize(input.size, input.size, { - kernel: kernel - }) - .crop(sharp.strategy.center); + kernel: kernel, + position: sharp.gravity.centre, + fit: 'cover' + }); } await image.jpeg().toFile(input.thPath); }; diff --git a/backend/routes/AdminRouter.ts b/backend/routes/AdminRouter.ts index 8c0b072f..3107cb61 100644 --- a/backend/routes/AdminRouter.ts +++ b/backend/routes/AdminRouter.ts @@ -84,6 +84,12 @@ export class AdminRouter { AdminMWs.updateShareSettings, RenderingMWs.renderOK ); + app.put('/api/settings/randomPhoto', + AuthenticationMWs.authenticate, + AuthenticationMWs.authorise(UserRoles.Admin), + AdminMWs.updateRandomPhotoSettings, + RenderingMWs.renderOK + ); app.put('/api/settings/basic', AuthenticationMWs.authenticate, AuthenticationMWs.authorise(UserRoles.Admin), diff --git a/backend/routes/GalleryRouter.ts b/backend/routes/GalleryRouter.ts index 242204c4..aec98334 100644 --- a/backend/routes/GalleryRouter.ts +++ b/backend/routes/GalleryRouter.ts @@ -10,6 +10,7 @@ export class GalleryRouter { this.addGetImageIcon(app); this.addGetImageThumbnail(app); this.addGetImage(app); + this.addRandom(app); this.addDirectoryList(app); this.addSearch(app); @@ -38,6 +39,17 @@ export class GalleryRouter { ); } + private static addRandom(app) { + app.get(['/api/gallery/random'], + AuthenticationMWs.authenticate, + AuthenticationMWs.authorise(UserRoles.Guest), + // TODO: authorize path + GalleryMWs.getRandomImage, + GalleryMWs.loadImage, + RenderingMWs.renderFile + ); + } + private static addGetImageThumbnail(app) { app.get('/api/gallery/content/:imagePath(*\.(jpg|bmp|png|gif|jpeg))/thumbnail/:size?', AuthenticationMWs.authenticate, diff --git a/common/QueryParams.ts b/common/QueryParams.ts new file mode 100644 index 00000000..edb66395 --- /dev/null +++ b/common/QueryParams.ts @@ -0,0 +1,13 @@ +export const QueryParams = { + gallery: { + random: { + directory: 'dir', + recursive: 'recursive', + orientation: 'orientation', + fromDate: 'fromDate', + toDate: 'toDate', + minResolution: 'fromRes', + maxResolution: 'toRes' + } + } +}; diff --git a/common/config/public/ConfigClass.ts b/common/config/public/ConfigClass.ts index c1654220..ce1d5c53 100644 --- a/common/config/public/ConfigClass.ts +++ b/common/config/public/ConfigClass.ts @@ -16,6 +16,10 @@ export module ClientConfig { passwordProtected: boolean; } + export interface RandomPhotoConfig { + enabled: boolean; + } + export interface MapConfig { enabled: boolean; googleApiKey: string; @@ -32,6 +36,7 @@ export module ClientConfig { Search: SearchConfig; Sharing: SharingConfig; Map: MapConfig; + RandomPhoto: RandomPhotoConfig; concurrentThumbnailGenerations: number; enableCache: boolean; enableOnScrollRendering: boolean; @@ -73,6 +78,9 @@ export class PublicConfigClass { enabled: true, googleApiKey: '' }, + RandomPhoto: { + enabled: true + }, concurrentThumbnailGenerations: 1, enableCache: true, enableOnScrollRendering: true, diff --git a/common/entities/RandomQueryDTO.ts b/common/entities/RandomQueryDTO.ts new file mode 100644 index 00000000..9e4ae641 --- /dev/null +++ b/common/entities/RandomQueryDTO.ts @@ -0,0 +1,13 @@ +export enum OrientationType { + any = 0, portrait = 1, landscape = 2 +} + +export interface RandomQueryDTO { + directory?: string; + recursive?: boolean; + orientation?: OrientationType; + fromDate?: string; + toDate?: string; + minResolution?: number; + maxResolution?: number; +} diff --git a/frontend/app/admin/admin.component.html b/frontend/app/admin/admin.component.html index e97d1364..1a096d13 100644 --- a/frontend/app/admin/admin.component.html +++ b/frontend/app/admin/admin.component.html @@ -48,6 +48,8 @@ + {{user.value.name}} - - + + + + diff --git a/frontend/app/gallery/fullscreen.service.ts b/frontend/app/gallery/fullscreen.service.ts index 1057938c..17720be6 100644 --- a/frontend/app/gallery/fullscreen.service.ts +++ b/frontend/app/gallery/fullscreen.service.ts @@ -8,7 +8,9 @@ export class FullScreenService { OnFullScreenChange = new Event(); public isFullScreenEnabled(): boolean { - return !!(document.fullscreenElement || document['mozFullScreenElement'] || document.webkitFullscreenElement); + return !!(document['fullscreenElement'] || + document['mozFullScreenElement'] || + document['webkitFullscreenElement']); } public showFullScreen(element: any) { @@ -37,8 +39,8 @@ export class FullScreenService { document.exitFullscreen(); } else if (document['mozCancelFullScreen']) { document['mozCancelFullScreen'](); - } else if (document.webkitExitFullscreen) { - document.webkitExitFullscreen(); + } else if (document['webkitExitFullscreen']) { + document['webkitExitFullscreen'](); } this.OnFullScreenChange.trigger(false); } diff --git a/frontend/app/gallery/gallery.component.html b/frontend/app/gallery/gallery.component.html index b0fca25c..f94bee76 100644 --- a/frontend/app/gallery/gallery.component.html +++ b/frontend/app/gallery/gallery.component.html @@ -2,7 +2,6 @@ - + +
  • + +
  • +
    +
    diff --git a/frontend/app/gallery/gallery.component.ts b/frontend/app/gallery/gallery.component.ts index dc704ea9..95349219 100644 --- a/frontend/app/gallery/gallery.component.ts +++ b/frontend/app/gallery/gallery.component.ts @@ -14,8 +14,6 @@ import {UserRoles} from '../../../common/entities/UserDTO'; import {interval} from 'rxjs'; import {ContentWrapper} from '../../../common/entities/ConentWrapper'; import {PageHelper} from '../model/page.helper'; -import {QueryService} from '../model/query.service'; -import {LightboxStates} from './lightbox/lightbox.gallery.component'; @Component({ selector: 'app-gallery', @@ -29,6 +27,8 @@ export class GalleryComponent implements OnInit, OnDestroy { public showSearchBar = false; public showShare = false; + public showRandomPhotoBuilder = false; + public directories: DirectoryDTO[] = []; public isPhotoWithLocation = false; private $counter; @@ -142,7 +142,7 @@ export class GalleryComponent implements OnInit, OnDestroy { } this.showSearchBar = Config.Client.Search.enabled && this._authService.isAuthorized(UserRoles.Guest); this.showShare = Config.Client.Sharing.enabled && this._authService.isAuthorized(UserRoles.User); - + this.showRandomPhotoBuilder = Config.Client.RandomPhoto.enabled && this._authService.isAuthorized(UserRoles.Guest); this.subscription.content = this._galleryService.content.subscribe(this.onContentChange); this.subscription.route = this._route.params.subscribe(this.onRoute); diff --git a/frontend/app/gallery/lightbox/lightbox.gallery.component.ts b/frontend/app/gallery/lightbox/lightbox.gallery.component.ts index ce3dda06..28378db7 100644 --- a/frontend/app/gallery/lightbox/lightbox.gallery.component.ts +++ b/frontend/app/gallery/lightbox/lightbox.gallery.component.ts @@ -228,18 +228,18 @@ export class GalleryLightboxComponent implements OnDestroy, OnInit { return; } const event: KeyboardEvent = window.event ? window.event : e; - switch (event.keyCode) { - case 37: + switch (event.key) { + case 'ArrowLeft': if (this.activePhotoId > 0) { this.prevImage(); } break; - case 39: + case 'ArrowRight': if (this.activePhotoId < this.gridPhotoQL.length - 1) { this.nextImage(); } break; - case 27: // escape + case 'Escape': // escape this.hide(); break; } diff --git a/frontend/app/gallery/map/lightbox/lightbox.map.gallery.component.html b/frontend/app/gallery/map/lightbox/lightbox.map.gallery.component.html index 8ad5fb8e..a01338eb 100644 --- a/frontend/app/gallery/map/lightbox/lightbox.map.gallery.component.html +++ b/frontend/app/gallery/map/lightbox/lightbox.map.gallery.component.html @@ -9,13 +9,14 @@ + [fitBounds]="true"> + (markerClick)="loadPreview(photo)" + [agmFitBounds]="true"> window.event : e; - switch (event.keyCode) { - case 27: // escape + switch (event.key) { + case 'Escape': // escape this.hide(); break; } diff --git a/frontend/app/gallery/map/map.gallery.component.html b/frontend/app/gallery/map/map.gallery.component.html index 9ecfd2f2..d86f04ea 100644 --- a/frontend/app/gallery/map/map.gallery.component.html +++ b/frontend/app/gallery/map/map.gallery.component.html @@ -8,11 +8,12 @@ [usePanning]="false" [draggable]="false" [zoom]="0" - [fitBounds]="latlngBounds"> + [fitBounds]="true"> + [longitude]="photo.longitude" + [agmFitBounds]="true">
    = []; - public latlngBounds: LatLngBounds; - @ViewChild('map') map: ElementRef; + @ViewChild('map') mapElement: ElementRef; height = null; @@ -37,32 +36,14 @@ export class GalleryMapComponent implements OnChanges, IRenderable, AfterViewIni }); - this.findPhotosBounds().catch(console.error); - } ngAfterViewInit() { setTimeout(() => { - this.height = this.map.nativeElement.clientHeight; + this.height = this.mapElement.nativeElement.clientHeight; }, 0); } - private async findPhotosBounds() { - await this.mapsAPILoader.load(); - if (!window['google']) { - return; - } - this.latlngBounds = new window['google'].maps.LatLngBounds(); - - for (const photo of this.mapPhotos) { - this.latlngBounds.extend(new window['google'].maps.LatLng(photo.latitude, photo.longitude)); - } - const clat = this.latlngBounds.getCenter().lat(); - const clng = this.latlngBounds.getCenter().lng(); - this.latlngBounds.extend(new window['google'].maps.LatLng(clat + 0.5, clng + 0.5)); - this.latlngBounds.extend(new window['google'].maps.LatLng(clat - 0.5, clng - 0.5)); - - } click() { this.mapLightbox.show(this.getDimension()); @@ -70,10 +51,10 @@ export class GalleryMapComponent implements OnChanges, IRenderable, AfterViewIni public getDimension(): Dimension { return { - top: this.map.nativeElement.offsetTop, - left: this.map.nativeElement.offsetLeft, - width: this.map.nativeElement.offsetWidth, - height: this.map.nativeElement.offsetHeight + top: this.mapElement.nativeElement.offsetTop, + left: this.mapElement.nativeElement.offsetLeft, + width: this.mapElement.nativeElement.offsetWidth, + height: this.mapElement.nativeElement.offsetHeight }; } } diff --git a/frontend/app/gallery/random-query-builder/random-query-builder.gallery.component.css b/frontend/app/gallery/random-query-builder/random-query-builder.gallery.component.css new file mode 100644 index 00000000..67eefffa --- /dev/null +++ b/frontend/app/gallery/random-query-builder/random-query-builder.gallery.component.css @@ -0,0 +1,27 @@ +.modal { + z-index: 9999; +} + +.full-width { + width: 100%; +} + +.row { + padding-top: 1px; + padding-bottom: 1px; +} + +a.disabled { + /* Make the disabled links grayish*/ + color: gray; + /* And disable the pointer events */ + pointer-events: none; +} + +a.dropdown-item { + padding:0.3rem 1.0rem 0.3rem 0.8rem; +} + +a.dropdown-item span{ + padding-right: 0.8rem; +} diff --git a/frontend/app/gallery/random-query-builder/random-query-builder.gallery.component.html b/frontend/app/gallery/random-query-builder/random-query-builder.gallery.component.html new file mode 100644 index 00000000..508b2c19 --- /dev/null +++ b/frontend/app/gallery/random-query-builder/random-query-builder.gallery.component.html @@ -0,0 +1,134 @@ + + + Random link + + + + + + + diff --git a/frontend/app/gallery/random-query-builder/random-query-builder.gallery.component.ts b/frontend/app/gallery/random-query-builder/random-query-builder.gallery.component.ts new file mode 100644 index 00000000..b1f003b2 --- /dev/null +++ b/frontend/app/gallery/random-query-builder/random-query-builder.gallery.component.ts @@ -0,0 +1,104 @@ +import {Component, OnDestroy, OnInit, TemplateRef} from '@angular/core'; +import {Utils} from '../../../../common/Utils'; +import {GalleryService} from '../gallery.service'; +import {ContentWrapper} from '../../../../common/entities/ConentWrapper'; +import {Config} from '../../../../common/config/public/Config'; +import {NotificationService} from '../../model/notification.service'; +import {DirectoryDTO} from '../../../../common/entities/DirectoryDTO'; +import {I18n} from '@ngx-translate/i18n-polyfill'; +import {BsModalService} from 'ngx-bootstrap/modal'; +import {BsModalRef} from 'ngx-bootstrap/modal/bs-modal-ref.service'; +import {OrientationType, RandomQueryDTO} from '../../../../common/entities/RandomQueryDTO'; +import {NetworkService} from '../../model/network/network.service'; + + +@Component({ + selector: 'app-gallery-random-query-builder', + templateUrl: './random-query-builder.gallery.component.html', + styleUrls: ['./random-query-builder.gallery.component.css'], +}) +export class RandomQueryBuilderGalleryComponent implements OnInit, OnDestroy { + + enabled = true; + url = ''; + + data: RandomQueryDTO = { + orientation: OrientationType.any, + directory: '', + recursive: true, + minResolution: null, + maxResolution: null, + toDate: null, + fromDate: null + }; + contentSubscription = null; + + OrientationType; + modalRef: BsModalRef; + + text = { + Yes: 'Yes', + No: 'No' + }; + + constructor(public _galleryService: GalleryService, + private _notification: NotificationService, + public i18n: I18n, + private modalService: BsModalService) { + this.OrientationType = OrientationType; + this.text.Yes = i18n('Yes'); + this.text.No = i18n('No'); + } + + + ngOnInit() { + this.contentSubscription = this._galleryService.content.subscribe((content: ContentWrapper) => { + this.enabled = !!content.directory; + if (!this.enabled) { + return; + } + this.data.directory = Utils.concatUrls((content.directory).path, (content.directory).name); + }); + } + + ngOnDestroy() { + if (this.contentSubscription !== null) { + this.contentSubscription.unsubscribe(); + } + } + + update() { + setTimeout(() => { + const data = Utils.clone(this.data); + for (const key of Object.keys(data)) { + if (!data[key]) { + delete data[key]; + } + } + this.url = NetworkService.buildUrl(Config.Client.publicUrl + '/api/gallery/random/', data); + }, 0); + } + + openModal(template: TemplateRef) { + if (!this.enabled) { + return; + } + if (this.modalRef) { + this.modalRef.hide(); + } + this.modalRef = this.modalService.show(template); + document.body.style.paddingRight = '0px'; + this.update(); + return false; + } + + onCopy() { + this._notification.success(this.i18n('Url has been copied to clipboard')); + } + + public hideModal() { + this.modalRef.hide(); + this.modalRef = null; + } + +} diff --git a/frontend/app/model/network/network.service.ts b/frontend/app/model/network/network.service.ts index 30a06e46..b2ef98c0 100644 --- a/frontend/app/model/network/network.service.ts +++ b/frontend/app/model/network/network.service.ts @@ -17,15 +17,7 @@ export class NetworkService { private slimLoadingBarService: SlimLoadingBarService) { } - public postJson(url: string, data: any = {}): Promise { - return this.callJson('post', url, data); - } - - public putJson(url: string, data: any = {}): Promise { - return this.callJson('put', url, data); - } - - public getJson(url: string, data?: { [key: string]: any }): Promise { + public static buildUrl(url: string, data?: { [key: string]: any }) { if (data) { const keys = Object.getOwnPropertyNames(data); if (keys.length > 0) { @@ -38,7 +30,19 @@ export class NetworkService { } } } - return this.callJson('get', url); + return url; + } + + public postJson(url: string, data: any = {}): Promise { + return this.callJson('post', url, data); + } + + public putJson(url: string, data: any = {}): Promise { + return this.callJson('put', url, data); + } + + public getJson(url: string, data?: { [key: string]: any }): Promise { + return this.callJson('get', NetworkService.buildUrl(url, data)); } public deleteJson(url: string): Promise { diff --git a/frontend/app/settings/random-photo/random-photo.settings.component.css b/frontend/app/settings/random-photo/random-photo.settings.component.css new file mode 100644 index 00000000..a5800fa2 --- /dev/null +++ b/frontend/app/settings/random-photo/random-photo.settings.component.css @@ -0,0 +1,3 @@ +.panel-info { + text-align: center; +} diff --git a/frontend/app/settings/random-photo/random-photo.settings.component.html b/frontend/app/settings/random-photo/random-photo.settings.component.html new file mode 100644 index 00000000..8075e7a4 --- /dev/null +++ b/frontend/app/settings/random-photo/random-photo.settings.component.html @@ -0,0 +1,45 @@ +
    +
    +
    + Random Photo settings +
    + + +
    +
    +
    + + + +
    + This feature enables you to generate 'random photo' urls. + That URL returns a photo random selected from your gallery. + You can use the url with 3rd party like random changing desktop background. +
    + +
    +
    + Random Photo is not supported with these settings +
    + + +
    +
    + +
    diff --git a/frontend/app/settings/random-photo/random-photo.settings.component.ts b/frontend/app/settings/random-photo/random-photo.settings.component.ts new file mode 100644 index 00000000..2f990715 --- /dev/null +++ b/frontend/app/settings/random-photo/random-photo.settings.component.ts @@ -0,0 +1,31 @@ +import {Component} from '@angular/core'; +import {SettingsComponent} from '../_abstract/abstract.settings.component'; +import {AuthenticationService} from '../../model/network/authentication.service'; +import {NavigationService} from '../../model/navigation.service'; +import {NotificationService} from '../../model/notification.service'; +import {ClientConfig} from '../../../../common/config/public/ConfigClass'; +import {RandomPhotoSettingsService} from './random-photo.settings.service'; +import {I18n} from '@ngx-translate/i18n-polyfill'; + +@Component({ + selector: 'app-settings-random-photo', + templateUrl: './random-photo.settings.component.html', + styleUrls: ['./random-photo.settings.component.css', + './../_abstract/abstract.settings.component.css'], + providers: [RandomPhotoSettingsService], +}) +export class RandomPhotoSettingsComponent extends SettingsComponent { + + constructor(_authService: AuthenticationService, + _navigation: NavigationService, + _settingsService: RandomPhotoSettingsService, + notification: NotificationService, + i18n: I18n) { + super(i18n('Random Photo'), _authService, _navigation, _settingsService, notification, i18n, s => s.Client.RandomPhoto); + } + + +} + + + diff --git a/frontend/app/settings/random-photo/random-photo.settings.service.ts b/frontend/app/settings/random-photo/random-photo.settings.service.ts new file mode 100644 index 00000000..4c585f9d --- /dev/null +++ b/frontend/app/settings/random-photo/random-photo.settings.service.ts @@ -0,0 +1,25 @@ +import {Injectable} from '@angular/core'; +import {NetworkService} from '../../model/network/network.service'; +import {DatabaseType} from '../../../../common/config/private/IPrivateConfig'; +import {ClientConfig} from '../../../../common/config/public/ConfigClass'; +import {SettingsService} from '../settings.service'; +import {AbstractSettingsService} from '../_abstract/abstract.settings.service'; + +@Injectable() +export class RandomPhotoSettingsService extends AbstractSettingsService { + constructor(private _networkService: NetworkService, + _settingsService: SettingsService) { + super(_settingsService); + + } + + + public isSupported(): boolean { + return this._settingsService.settings.value.Server.database.type !== DatabaseType.memory; + } + + public updateSettings(settings: ClientConfig.SharingConfig): Promise { + return this._networkService.putJson('/settings/randomPhoto', {settings: settings}); + } + +} diff --git a/frontend/app/settings/settings.service.ts b/frontend/app/settings/settings.service.ts index b64c224b..01010de6 100644 --- a/frontend/app/settings/settings.service.ts +++ b/frontend/app/settings/settings.service.ts @@ -33,6 +33,9 @@ export class SettingsService { enabled: true, googleApiKey: '' }, + RandomPhoto: { + enabled: true + }, urlBase: '', publicUrl: '', applicationTitle: '', diff --git a/package.json b/package.json index 4f4a88ab..6acd42e8 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "ng": "ng", "lint": "ng lint", "e2e": "ng e2e", - "run-dev": "ng build --aot -w --output-path=./dist --locale en --i18n-format xlf --i18n-file frontend/translate/messages.en.xlf --missing-translation warning", + "run-dev": "ng build --aot --watch --output-path=./dist --i18n-locale en --i18n-format xlf --i18n-file frontend/translate/messages.en.xlf --i18n-missing-translation warning", "update-translation": "gulp update-translation", "add-translation": "gulp add-translation" }, @@ -33,46 +33,46 @@ "cookie-parser": "1.4.3", "cookie-session": "2.0.0-beta.3", "ejs": "2.6.1", - "express": "4.16.3", - "jimp": "0.2.28", + "express": "4.16.4", + "jimp": "0.5.4", "locale": "0.1.0", "reflect-metadata": "0.1.12", "sqlite3": "4.0.2", "ts-exif-parser": "0.1.24", "ts-node-iptc": "1.0.10", "typeconfig": "1.0.6", - "typeorm": "0.2.7", + "typeorm": "0.2.8", "winston": "2.4.2" }, "devDependencies": { - "@agm/core": "1.0.0-beta.3", - "@angular-devkit/build-angular": "0.7.1", - "@angular-devkit/build-optimizer": "0.7.1", - "@angular/animations": "6.1.0", - "@angular/cli": "6.1.1", - "@angular/common": "6.1.0", - "@angular/compiler": "6.1.0", - "@angular/compiler-cli": "6.1.0", - "@angular/core": "6.1.0", - "@angular/forms": "6.1.0", - "@angular/http": "6.1.0", - "@angular/language-service": "^6.1.0", - "@angular/platform-browser": "6.1.0", - "@angular/platform-browser-dynamic": "6.1.0", - "@angular/router": "6.1.0", + "@agm/core": "1.0.0-beta.5", + "@angular-devkit/build-angular": "0.10.2", + "@angular-devkit/build-optimizer": "0.10.2", + "@angular/animations": "7.0.0", + "@angular/cli": "7.0.2", + "@angular/common": "7.0.0", + "@angular/compiler": "7.0.0", + "@angular/compiler-cli": "7.0.0", + "@angular/core": "7.0.0", + "@angular/forms": "7.0.0", + "@angular/http": "7.0.0", + "@angular/language-service": "7.0.0", + "@angular/platform-browser": "7.0.0", + "@angular/platform-browser-dynamic": "7.0.0", + "@angular/router": "7.0.0", "@ngx-translate/i18n-polyfill": "1.0.0", - "@types/bcryptjs": "2.4.1", - "@types/chai": "4.1.4", - "@types/cookie-session": "2.0.35", + "@types/bcryptjs": "2.4.2", + "@types/chai": "4.1.6", + "@types/cookie-session": "2.0.36", "@types/express": "4.16.0", - "@types/gm": "1.18.0", - "@types/jasmine": "2.8.8", - "@types/node": "10.5.4", - "@types/sharp": "0.17.9", - "@types/winston": "^2.3.9", + "@types/gm": "1.18.1", + "@types/jasmine": "2.8.9", + "@types/node": "10.12.0", + "@types/sharp": "0.21.0", + "@types/winston": "2.3.9", "bootstrap": "4.1.3", - "chai": "4.1.2", - "codelyzer": "4.4.2", + "chai": "4.2.0", + "codelyzer": "4.5.0", "core-js": "2.5.7", "ejs-loader": "0.3.1", "gulp": "3.9.1", @@ -81,45 +81,48 @@ "gulp-zip": "4.2.0", "hammerjs": "2.0.8", "intl": "1.2.5", - "jasmine-core": "3.1.0", + "jasmine-core": "3.2.1", "jasmine-spec-reporter": "4.2.1", - "jw-bootstrap-switch-ng2": "1.0.10", - "karma": "2.0.5", + "jw-bootstrap-switch-ng2": "2.0.2", + "karma": "3.0.0", "karma-chrome-launcher": "2.2.0", "karma-cli": "1.0.1", - "karma-coverage-istanbul-reporter": "2.0.1", + "karma-coverage-istanbul-reporter": "2.0.4", "karma-jasmine": "1.1.2", - "karma-jasmine-html-reporter": "1.2.0", + "karma-jasmine-html-reporter": "1.3.1", "karma-remap-istanbul": "0.6.0", "karma-systemjs": "0.16.0", - "merge2": "1.2.2", + "merge2": "1.2.3", "mocha": "5.2.0", "ng2-cookies": "1.0.12", "ng2-slim-loading-bar": "4.0.0", "ngx-bootstrap": "3.0.1", - "ngx-clipboard": "11.1.1", - "ngx-toastr": "8.10.0", + "ngx-clipboard": "11.1.9", + "ngx-toastr": "9.1.1", "open-iconic": "1.1.1", - "protractor": "5.4.0", - "remap-istanbul": "0.11.1", + "protractor": "5.4.1", + "remap-istanbul": "0.12.0", "rimraf": "2.6.2", "run-sequence": "2.2.1", - "rxjs": "6.2.2", - "rxjs-compat": "^6.2.2", + "rxjs": "6.3.3", + "rxjs-compat": "^6.3.3", "ts-helpers": "1.1.2", - "ts-node": "7.0.0", + "ts-node": "7.0.1", "tslint": "5.11.0", - "typescript": "2.9.2", + "typescript": "3.1.3", "xlf-google-translate": "1.0.0-beta.11", "zone.js": "0.8.26" }, + "resolutions": { + "natives": "1.1.3" + }, "optionalDependencies": { "mysql": "2.16.0", - "bcrypt": "3.0.0", + "bcrypt": "3.0.2", "gm": "1.23.1", - "sharp": "0.20.5" + "sharp": "0.21.0" }, "engines": { - "node": ">= 6.9 <10.0" + "node": ">= 6.9 <=10.0" } }